gled-ui 2.31.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Painting a canvas in a fixed order, and the grid and arrows drawn on it.

use egui::emath::TSTransform;
use egui::{Color32, Id, LayerId, Painter, Pos2, Rect, Shape, Stroke, StrokeKind, Ui, pos2};

/// Shapes held back until the canvas is ready for them.
///
/// The canvas paints into one layer and lets the order of the calls decide
/// what covers what: egui gives a layer's sublayers no order among themselves,
/// and a sublayer of the scene's own sublayer is undefined altogether. The
/// methods mirror the [`Painter`] ones they replace.
#[derive(Default)]
pub(crate) struct Shapes(Vec<Shape>);

impl Shapes {
    pub(crate) fn add(&mut self, shape: impl Into<Shape>) {
        self.0.push(shape.into());
    }

    pub(crate) fn line_segment(&mut self, points: [Pos2; 2], stroke: impl Into<Stroke>) {
        self.add(Shape::line_segment(points, stroke));
    }

    pub(crate) fn rect(
        &mut self,
        rect: Rect,
        corner_radius: impl Into<egui::CornerRadius>,
        fill_color: impl Into<Color32>,
        stroke: impl Into<Stroke>,
        stroke_kind: StrokeKind,
    ) {
        self.add(egui::epaint::RectShape::new(
            rect,
            corner_radius,
            fill_color,
            stroke,
            stroke_kind,
        ));
    }

    pub(crate) fn rect_stroke(
        &mut self,
        rect: Rect,
        corner_radius: impl Into<egui::CornerRadius>,
        stroke: impl Into<Stroke>,
        stroke_kind: StrokeKind,
    ) {
        self.add(egui::epaint::RectShape::stroke(
            rect,
            corner_radius,
            stroke,
            stroke_kind,
        ));
    }

    pub(crate) fn circle(
        &mut self,
        center: Pos2,
        radius: f32,
        fill_color: impl Into<Color32>,
        stroke: impl Into<Stroke>,
    ) {
        self.add(egui::epaint::CircleShape {
            center,
            radius,
            fill: fill_color.into(),
            stroke: stroke.into(),
        });
    }

    pub(crate) fn into_shape(self) -> Shape {
        Shape::Vec(self.0)
    }
}

/// The one layer a canvas paints into. It hangs off the host ui rather than
/// off the scene, so a canvas inside a modal draws above the modal, and the
/// nesting stays one level deep.
pub(crate) fn canvas_painter(clip_rect: Rect, child_ui: &Ui, host_layer: LayerId) -> Painter {
    // salted with the host, so two canvases in one window keep their own
    let layer = LayerId::new(host_layer.order, Id::new(("editor_painter", host_layer)));
    child_ui.ctx().set_sublayer(host_layer, layer);
    let mut painter = child_ui.ctx().layer_painter(layer);
    painter.set_clip_rect(clip_rect);
    painter
}

fn draw_grid(
    edge_painter: &mut Shapes,
    rect: Rect,
    tst: &TSTransform,
    spacing: f32,
    stroke: Stroke,
) {
    let mut x = (rect.left() / spacing).floor() * spacing;
    while x <= rect.right() {
        edge_painter.line_segment(
            [*tst * pos2(x, rect.top()), *tst * pos2(x, rect.bottom())],
            stroke,
        );
        x += spacing;
    }

    let mut y = (rect.top() / spacing).floor() * spacing;
    while y <= rect.bottom() {
        edge_painter.line_segment(
            [*tst * pos2(rect.left(), y), *tst * pos2(rect.right(), y)],
            stroke,
        );
        y += spacing;
    }
}

/// How far apart grid lines have to be on screen to read as a grid rather
/// than a moiré, in screen pixels.
const MIN_GRID_PX: f32 = 6.0;

/// One grid per `(spacing, stroke)` over `rect`, both in scene units, each
/// only while its lines are far enough apart on screen to read as a grid.
pub(crate) fn draw_adaptive_grid(
    shapes: &mut Shapes,
    rect: Rect,
    transform: &TSTransform,
    levels: &[(f32, Stroke)],
) {
    for (spacing, stroke) in levels {
        if spacing * transform.scaling >= MIN_GRID_PX {
            draw_grid(shapes, rect, transform, *spacing, *stroke);
        }
    }
}

pub(crate) fn draw_edge(edge_painter: &mut Shapes, prev_terminal: &Pos2, next_terminal: &Pos2) {
    edge_painter.line_segment([*prev_terminal, *next_terminal], (1.0, Color32::WHITE));

    // a triangle arrowhead rotated to match the direction of the line, its tip
    // on the end of it, where the signal enters the next fixture
    let direction = (*next_terminal - *prev_terminal).normalized();
    let perp = egui::vec2(-direction.y, direction.x);
    const ARROW_SIZE: f32 = 8.0;
    let arrow_points = [
        *next_terminal,
        *next_terminal - direction * ARROW_SIZE + perp * (ARROW_SIZE / 2.0),
        *next_terminal - direction * ARROW_SIZE - perp * (ARROW_SIZE / 2.0),
    ];

    edge_painter.add(Shape::convex_polygon(
        Vec::from(arrow_points),
        Color32::WHITE,
        Stroke::NONE,
    ));
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Regression: the canvas hung its painting layers off the scene's own
    /// sublayer. egui leaves two levels of nesting undefined, and whenever the
    /// ordering came out the other way the whole canvas went missing for a
    /// frame.
    #[test]
    fn the_canvas_paints_under_a_layer_that_is_not_itself_a_sublayer() {
        egui::__run_test_ui(|ui| {
            // what the scene does with the ui it is given
            let scene = egui::LayerId::new(ui.layer_id().order, ui.id().with("scene_area"));
            ui.ctx().set_sublayer(ui.layer_id(), scene);
            let host = ui.layer_id();

            let painter = canvas_painter(ui.clip_rect(), ui, host);

            let parent = ui
                .ctx()
                .memory(|memory| memory.areas().parent_layer(painter.layer_id()));
            assert_eq!(parent, Some(host));
            assert_eq!(
                ui.ctx().memory(|memory| memory.areas().parent_layer(host)),
                None,
                "the host is a layer of its own, so the nesting stops at one level"
            );
        });
    }

    /// The arrowhead points at the fixture the signal enters, so its tip sits
    /// on the end of the edge rather than halfway along it.
    #[test]
    fn an_edge_carries_its_arrowhead_at_the_end() {
        let mut shapes = Shapes::default();
        let (from, to) = (pos2(0.0, 0.0), pos2(100.0, 0.0));

        draw_edge(&mut shapes, &from, &to);

        let head = shapes
            .0
            .iter()
            .find_map(|shape| match shape {
                Shape::Path(path) => Some(path.points.clone()),
                _ => None,
            })
            .expect("an arrowhead");
        assert_eq!(head[0], to, "the tip is on the end");
        assert!(
            head[1..].iter().all(|point| point.x < to.x),
            "the barbs trail behind it: {head:?}"
        );
    }
}