use iced::{
Color, Element, Event, Length, Point, Rectangle, Renderer, Theme, mouse,
widget::{
button, canvas,
canvas::{Action, Frame, Geometry, Path, Program, Stroke, Text},
column, row, text,
},
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AdsrMessage {
Change(usize, f32),
Release(usize),
Reset(usize),
Assign(usize),
}
pub fn adsr_envelope<'a>(
values: [f32; 4],
ranges: [(f32, f32); 4],
steps: [f32; 4],
assigned: [bool; 4],
) -> Element<'a, AdsrMessage> {
let graph = Envelope {
values,
ranges,
steps,
assigned,
};
let labels = row((0..4).map(|i| {
let value = if i == 2 {
format!("{:.0}%", values[i] * 100.0)
} else {
format!("{:.2}s", values[i])
};
button(column![
text(["Attack", "Decay", "Sustain", "Release"][i]).size(10),
text(value).size(10)
])
.padding(2)
.width(Length::Fill)
.style(button::text)
.on_press(AdsrMessage::Assign(i))
.into()
}))
.width(210);
column![canvas(graph).width(210).height(115), labels]
.spacing(3)
.into()
}
struct Envelope {
values: [f32; 4],
ranges: [(f32, f32); 4],
steps: [f32; 4],
assigned: [bool; 4],
}
#[derive(Default)]
struct DragState {
drag: Option<(usize, Point, f32)>,
}
impl Envelope {
fn time_position(value: f32, range: (f32, f32)) -> f32 {
let norm = ((value - range.0) / (range.1 - range.0).max(f32::EPSILON)).clamp(0.0, 1.0);
(1.0 + norm * 100.0).ln() / 101.0_f32.ln()
}
fn time_value(position: f32, range: (f32, f32)) -> f32 {
let norm = (101.0_f32.powf(position.clamp(0.0, 1.0)) - 1.0) / 100.0;
range.0 + norm * (range.1 - range.0)
}
fn points(&self) -> [Point; 5] {
let attack = 12.0 + 28.0 * Self::time_position(self.values[0], self.ranges[0]);
let decay = attack + 12.0 + 28.0 * Self::time_position(self.values[1], self.ranges[1]);
let sustain_range = self.ranges[2];
let sustain_norm = ((self.values[2] - sustain_range.0)
/ (sustain_range.1 - sustain_range.0).max(f32::EPSILON))
.clamp(0.0, 1.0);
let sustain = 94.0 - sustain_norm * 72.0;
[
Point::new(12.0, 94.0),
Point::new(attack, 22.0),
Point::new(decay, sustain),
Point::new(decay + 36.0, sustain),
Point::new(
decay + 48.0 + 28.0 * Self::time_position(self.values[3], self.ranges[3]),
94.0,
),
]
}
fn hit(&self, point: Point) -> Option<usize> {
self.points()[1..]
.iter()
.position(|p| p.distance(point) <= 9.0)
}
}
impl Program<AdsrMessage> for Envelope {
type State = DragState;
fn update(
&self,
state: &mut DragState,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<Action<AdsrMessage>> {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let point = cursor.position_in(bounds)?;
let i = self.hit(point)?;
state.drag = Some((i, point, self.values[i]));
Some(Action::capture())
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
let i = self.hit(cursor.position_in(bounds)?)?;
Some(Action::publish(AdsrMessage::Reset(i)).and_capture())
}
Event::Mouse(mouse::Event::CursorMoved { position }) => {
let (i, start, initial) = state.drag?;
let point = Point::new(position.x - bounds.x, position.y - bounds.y);
let range = self.ranges[i];
let value = if i == 2 {
let norm = (initial - range.0) / (range.1 - range.0).max(f32::EPSILON)
- (point.y - start.y) / 72.0;
range.0 + norm * (range.1 - range.0)
} else {
Self::time_value(
Self::time_position(initial, range) + (point.x - start.x) / 28.0,
range,
)
};
let step = self.steps[i].max(f32::EPSILON);
let value = ((value / step).round() * step).clamp(range.0, range.1);
Some(Action::publish(AdsrMessage::Change(i, value)).and_capture())
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Window(iced::window::Event::Unfocused) => {
let (i, _, _) = state.drag.take()?;
Some(Action::publish(AdsrMessage::Release(i)).and_capture())
}
_ => None,
}
}
fn draw(
&self,
_state: &DragState,
renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<Geometry> {
let mut frame = Frame::new(renderer, bounds.size());
let palette = theme.extended_palette();
let ink = palette.background.base.text;
let accent = palette.primary.base.color;
let points = self.points();
let shape = Path::new(|p| {
p.move_to(points[0]);
for point in &points[1..] {
p.line_to(*point);
}
p.close();
});
frame.fill(&shape, Color { a: 0.18, ..accent });
let curve = Path::new(|p| {
p.move_to(points[0]);
for point in &points[1..] {
p.line_to(*point);
}
});
frame.stroke(&curve, Stroke::default().with_color(accent).with_width(2.0));
frame.stroke(
&Path::new(|p| {
p.move_to(Point::new(12.0, 16.0));
p.line_to(points[0]);
p.line_to(Point::new(205.0, 94.0));
}),
Stroke::default().with_color(ink),
);
for (i, point) in points[1..].iter().enumerate() {
let mut y = point.y + 7.0;
while y < 101.0 {
frame.stroke(
&Path::line(
Point::new(point.x, y),
Point::new(point.x, (y + 2.0).min(101.0)),
),
Stroke::default().with_color(Color { a: 0.4, ..ink }),
);
y += 5.0;
}
frame.fill(
&Path::circle(*point, 4.0),
if self.assigned[i] {
Color::from_rgb(1.0, 0.83, 0.10)
} else {
accent
},
);
}
for (label, position) in [
("level", Point::new(0.0, 0.0)),
("time", Point::new(183.0, 102.0)),
] {
frame.fill_text(Text {
content: label.into(),
position,
color: ink,
size: 10.0.into(),
..Text::default()
});
}
vec![frame.into_geometry()]
}
fn mouse_interaction(
&self,
state: &DragState,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> mouse::Interaction {
if state.drag.is_some() {
mouse::Interaction::Grabbing
} else if cursor
.position_in(bounds)
.and_then(|p| self.hit(p))
.is_some()
{
mouse::Interaction::Grab
} else {
mouse::Interaction::default()
}
}
}