use egui::{
Color32, CornerRadius, Pos2, Rect, Sense, Shape, Stroke, Ui, Vec2,
epaint::{CircleShape, PathShape, QuadraticBezierShape},
};
use emath::RectTransform;
use gled_model::asset::curve::{Curve, point::CurvePoint, pos::CurvePos};
const CONTROL_POINT_RADIUS: f32 = 8.0;
pub(crate) fn pos2(pos: CurvePos) -> Pos2 {
Pos2::new(pos.x, pos.y)
}
pub(crate) fn curve_pos(pos: Pos2) -> CurvePos {
CurvePos::new(pos.x, pos.y)
}
trait CurvePointUi {
fn point_rect(&self, to_screen: RectTransform) -> Rect;
fn shape(&self, to_screen: RectTransform, stroke: Stroke) -> Shape;
fn screen_pos(&self, to_screen: RectTransform) -> Pos2;
fn set_screen_pos(&mut self, to_screen: RectTransform, screen_pos: Pos2);
}
impl CurvePointUi for CurvePoint {
fn point_rect(&self, to_screen: RectTransform) -> Rect {
Rect::from_center_size(
self.screen_pos(to_screen),
Vec2::splat(2.0 * CONTROL_POINT_RADIUS),
)
}
fn shape(&self, to_screen: RectTransform, stroke: Stroke) -> Shape {
match self {
CurvePoint::First(_) => {
let point_rect = self.point_rect(to_screen);
Shape::convex_polygon(
vec![
point_rect.center_top(),
point_rect.center_bottom(),
point_rect.right_center(),
],
Color32::TRANSPARENT,
stroke,
)
}
CurvePoint::Inner(_) => Shape::rect_stroke(
self.point_rect(to_screen),
CornerRadius::default(),
stroke,
egui::StrokeKind::Inside,
),
CurvePoint::Bezier(_) => {
Shape::circle_stroke(self.screen_pos(to_screen), CONTROL_POINT_RADIUS, stroke)
}
CurvePoint::Last(_) => {
let point_rect = self.point_rect(to_screen);
Shape::convex_polygon(
vec![
point_rect.center_top(),
point_rect.center_bottom(),
point_rect.left_center(),
],
Color32::TRANSPARENT,
stroke,
)
}
}
}
fn screen_pos(&self, to_screen: RectTransform) -> Pos2 {
to_screen.transform_pos(pos2(self.pos()))
}
fn set_screen_pos(&mut self, to_screen: RectTransform, screen_pos: Pos2) {
self.set_pos(curve_pos(
to_screen.inverse().transform_pos_clamped(screen_pos),
));
}
}
pub(crate) trait CurveEditor {
fn draw_curve(
&mut self,
ui: &mut Ui,
edit_mode: bool,
beat_progression: Option<f32>,
rect: Rect,
) -> bool;
}
impl CurveEditor for Curve {
fn draw_curve(
&mut self,
ui: &mut Ui,
edit_mode: bool,
beat_progression: Option<f32>,
rect: Rect,
) -> bool {
let mut changed = false;
let to_screen = emath::RectTransform::from_to(
Rect::from_min_size(Pos2::ZERO, Vec2::new(4.0, 1.0)),
rect,
);
let mut outer_change = None;
let mut bezier_to_line = None;
let mut remove_point = None;
let painter = ui.painter().with_clip_rect(rect);
for i in 0..=4 {
let stroke = Stroke::new(
match i {
0 | 4 => 1.0,
_ => 0.5,
},
Color32::GRAY,
);
painter.add(PathShape::line(
vec![
to_screen.transform_pos(Pos2::new(i as f32, 0.0)),
to_screen.transform_pos(Pos2::new(i as f32, 1.0)),
],
stroke,
));
painter.add(PathShape::line(
vec![
to_screen.transform_pos(Pos2::new(0.0, i as f32 * 0.25)),
to_screen.transform_pos(Pos2::new(4.0, i as f32 * 0.25)),
],
stroke,
));
}
if edit_mode {
let new_point_id = ui.make_persistent_id(format!("points: {}", self.points.len()));
let new_point_response = ui.interact(*to_screen.to(), new_point_id, Sense::click());
if (new_point_response.double_clicked()
|| new_point_response.clicked_by(egui::PointerButton::Secondary))
&& let Some((pos, i, before, after)) =
new_point_response.interact_pointer_pos().and_then(|pos| {
self.points
.iter()
.position(|point| point.screen_pos(to_screen).x > pos.x)
.and_then(|i| {
self.points.get(i).and_then(|after| {
self.points
.get(i - 1)
.map(|before| (pos, i, *before, *after))
})
})
})
{
if before.is_inner() || before.is_outer() {
self.points.insert(
i,
CurvePoint::Inner(curve_pos(to_screen.inverse().transform_pos(pos))),
);
self.points.insert(
i,
CurvePoint::Bezier(curve_pos(to_screen.inverse().transform_pos(
Pos2::new(
(before.screen_pos(to_screen).x + pos.x) / 2.0,
(before.screen_pos(to_screen).y + pos.y) / 2.0,
),
))),
);
bezier_to_line = Some(i + 2);
changed = true;
} else {
bezier_to_line = Some(i - 1);
self.points.insert(
i,
CurvePoint::Bezier(curve_pos(to_screen.inverse().transform_pos(
Pos2::new(
(after.screen_pos(to_screen).x + pos.x) / 2.0,
(after.screen_pos(to_screen).y + pos.y) / 2.0,
),
))),
);
self.points.insert(
i,
CurvePoint::Inner(curve_pos(to_screen.inverse().transform_pos(pos))),
);
changed = true;
}
}
let x_limits = { 0..self.points.len() }
.map(|i| {
if i == 0 {
return None;
}
self.points
.get(i - 1)
.map(|point| point.screen_pos(to_screen).x)
.and_then(|before| {
self.points
.get(i + 1)
.map(|point| (before, point.screen_pos(to_screen).x))
})
})
.collect::<Vec<_>>();
let control_point_shapes: Vec<Shape> = self
.points
.iter_mut()
.enumerate()
.map(|(i, point)| {
let point_id = ui.make_persistent_id(format!("point: {i}"));
let point_response = ui.interact(
point.point_rect(to_screen),
point_id,
Sense::click_and_drag(),
);
if point_response.double_clicked()
|| point_response.clicked_by(egui::PointerButton::Secondary)
{
if point.is_outer() {
self.linked = !self.linked;
changed = true;
if self.linked {
outer_change = Some((i, point.pos()));
}
} else if point.is_bezier() {
bezier_to_line = Some(i);
} else if point.is_inner() {
remove_point = Some(i);
}
} else if point_response.dragged() {
let mut new_screen_pos =
point.screen_pos(to_screen) + point_response.drag_delta();
if let Some(x_limit) = x_limits.get(i).and_then(|x_limit| *x_limit) {
new_screen_pos.x = new_screen_pos.x.clamp(x_limit.0, x_limit.1)
}
point.set_screen_pos(to_screen, new_screen_pos);
if point_response.dragged() && point.is_outer() && self.linked {
outer_change = Some((i, point.pos()));
}
changed = true;
}
let stroke = if point.is_outer() && self.linked {
let mut stroke = ui.style().interact(&point_response).fg_stroke;
stroke.color = Color32::LIGHT_BLUE;
stroke
} else {
ui.style().interact(&point_response).fg_stroke
};
point.shape(to_screen, stroke)
})
.collect();
if let Some((i, pos)) = outer_change {
let i = self.points.len() - i - 1;
if let Some(point) = self.points.get_mut(i) {
point.set_pos(pos);
changed = true;
}
}
if let Some(i) = remove_point {
self.points.remove(i);
self.points.remove(i);
bezier_to_line = Some(i - 1);
changed = true;
}
if let Some(i) = bezier_to_line
&& let (Some(before), Some(after), Some(point)) = (
self.points.get(i - 1).map(|point| point.pos()),
self.points.get(i + 1).map(|point| point.pos()),
self.points.get_mut(i),
)
{
point.set_pos(CurvePos::new(
(before.x + after.x) / 2.0,
(before.y + after.y) / 2.0,
));
changed = true;
}
painter.extend(control_point_shapes);
}
let points_in_screen: Vec<Pos2> = self
.points
.iter()
.map(|p| p.screen_pos(to_screen))
.collect();
for i in 0..(points_in_screen.len() - 1) / 2 {
let shape = QuadraticBezierShape::from_points_stroke(
[
points_in_screen[i * 2],
points_in_screen[i * 2 + 1],
points_in_screen[i * 2 + 2],
],
false,
Color32::TRANSPARENT,
Stroke::new(1.0, Color32::from_rgb(25, 200, 100)),
);
painter.add(shape);
}
if edit_mode {
painter.add(PathShape::line(
points_in_screen,
Stroke::new(1.0, Color32::RED.linear_multiply(0.25)),
));
}
if let Some(beat_progression) = beat_progression {
let beat_progression = beat_progression % 4.0;
let color = Color32::from_rgb(0, 255, 230);
painter.add(PathShape::line(
vec![
to_screen.transform_pos(Pos2::new(beat_progression, 0.0)),
to_screen.transform_pos(Pos2::new(beat_progression, 1.0)),
],
Stroke::new(1.0, color),
));
painter.add(CircleShape::filled(
to_screen.transform_pos(Pos2::new(
beat_progression,
1.0 - self.value(beat_progression),
)),
2.0,
color,
));
}
changed
}
}