gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Drawing and editing curves with egui. The curve itself is model data in
//! `storage::asset::curve`; this is its view.

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;

/// A curve position in egui's coordinates.
pub(crate) fn pos2(pos: CurvePos) -> Pos2 {
    Pos2::new(pos.x, pos.y)
}

/// An egui position as a curve position.
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),
        ));
    }
}

/// Draws a curve and, in edit mode, lets the user move, add and remove points.
pub(crate) trait CurveEditor {
    /// Returns whether the curve changed.
    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
    }
}