gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! How assets look in asset trees and pickers.

use gled_protocol::assets::KnownAsset;
use std::sync::Arc;

use egui::{
    Color32, Context, Id, Mesh, Rect, Shape, TextureHandle, Vec2,
    epaint::{Vertex, WHITE_UV},
};
use std::path::Path;

use crate::canvas::images::Images;
use crate::canvas::view::fit_into;
use crate::ui::{color::ToColor32, curve_editor::CurveEditor, pills::show_pills};
use gled_model::asset::{
    animation::Animation, curve::Curve, midi_controller::MidiController,
    output_device::OutputDevice, palette::Palette, project::Project, scene::Scene,
};
use gled_model::patching::definition::FixtureDefinition;

/// An asset kind the UI can list. `show` draws a small preview at the right
/// end of `rect`; most kinds have none.
pub(crate) trait AssetUi: KnownAsset {
    fn show(&self, ui: &mut egui::Ui, rect: Rect) {
        let _ = (ui, rect);
    }
}

impl AssetUi for Animation {}
impl AssetUi for MidiController {}
impl AssetUi for OutputDevice {}
impl AssetUi for Project {}

/// The definition's photo where it has one, so the lamp is recognised at a
/// glance; otherwise its pixels as dots, fitted into the right end of the
/// row, so a bar, a ring and a matrix can be told apart in the list.
impl AssetUi for FixtureDefinition {
    fn show(&self, ui: &mut egui::Ui, rect: Rect) {
        const WIDTH: f32 = 64.0;
        const DOT: f32 = 1.2;
        let area =
            Rect::from_min_max(egui::pos2(rect.max.x - WIDTH, rect.min.y), rect.max).shrink(1.0);
        if let Some(texture) = self
            .photo
            .as_ref()
            .and_then(|photo| list_photo(ui.ctx(), &photo.file))
        {
            ui.painter_at(area).image(
                texture.id(),
                fit_into(texture.size_vec2(), area),
                Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
                Color32::WHITE,
            );
            return;
        }
        let Some((min, max)) = self.pixel_bounds_m() else {
            return;
        };
        let extent = Vec2::new(max[0] - min[0], max[1] - min[1]);
        let scale = (area.width() / extent.x.max(f32::EPSILON))
            .min(area.height() / extent.y.max(f32::EPSILON));
        let centre = egui::pos2((min[0] + max[0]) / 2.0, (min[1] + max[1]) / 2.0);
        let painter = ui.painter_at(area);
        for pixel in &self.pixels {
            let offset = Vec2::new(
                pixel.position_m[0] - centre.x,
                pixel.position_m[1] - centre.y,
            );
            painter.circle_filled(area.center() + offset * scale, DOT, Color32::from_gray(200));
        }
    }
}

/// A photo for the asset list, decoded once per file, off the UI thread. The
/// list has no state of its own, so the decoded photos are kept with the egui
/// context; they are taken out while one loads, as loading a texture needs
/// the context too.
fn list_photo(ctx: &Context, file: &Path) -> Option<TextureHandle> {
    let id = Id::new("fixture definition list photos");
    let mut images = ctx
        .data_mut(|data| data.remove_temp::<Images>(id))
        .unwrap_or_default();
    let texture = images.texture(ctx, file);
    ctx.data_mut(|data| data.insert_temp(id, images));
    texture
}

impl AssetUi for Curve {
    fn show(&self, ui: &mut egui::Ui, rect: Rect) {
        let mut min = rect.min;
        min.x = rect.max.x - 64.0;
        let rect = Rect::from_min_max(min, rect.max);

        let mut curve = self.clone();
        curve.draw_curve(ui, false, None, rect);
    }
}

impl AssetUi for Palette {
    fn show(&self, ui: &mut egui::Ui, rect: Rect) {
        let mesh = {
            const COLOR_GRADIENT_RATIO: f32 = 0.35;
            const GAP: f32 = 2.0;
            const BORDER: f32 = 1.0;
            const HEIGHT: f32 = 18.0;
            let available_width = rect.width() - GAP - 4.0 * BORDER;
            let color_width = available_width * COLOR_GRADIENT_RATIO;
            let gradient_with: f32 = available_width - color_width;
            let width_per_gradient_color = gradient_with / 16.0;
            let start_pos = rect.right_center();

            let mut mesh = Mesh::default();
            mesh.add_colored_rect(
                Rect::from_min_max(
                    start_pos
                        - Vec2::new(
                            BORDER + color_width + BORDER + GAP + BORDER + gradient_with + BORDER,
                            HEIGHT / 2.0,
                        ),
                    start_pos - Vec2::new(GAP + BORDER + gradient_with + BORDER, -HEIGHT / 2.0),
                ),
                Color32::BLACK,
            );
            let index = mesh.vertices.len() as u32;
            mesh.add_triangle(index, index + 1, index + 2);
            mesh.add_triangle(index + 3, index + 4, index + 5);
            let primary_color = self.primary.color32();
            // Top left
            mesh.vertices.push(Vertex {
                pos: start_pos
                    - Vec2::new(
                        color_width + BORDER + GAP + BORDER + gradient_with + BORDER,
                        HEIGHT / 2.0 - BORDER,
                    ),
                uv: WHITE_UV,
                color: primary_color,
            });
            // Top right
            mesh.vertices.push(Vertex {
                pos: start_pos
                    - Vec2::new(
                        BORDER + GAP + BORDER + gradient_with + BORDER,
                        HEIGHT / 2.0 - BORDER,
                    ),
                uv: WHITE_UV,
                color: primary_color,
            });
            // Bottom left
            mesh.vertices.push(Vertex {
                pos: start_pos
                    - Vec2::new(
                        color_width + BORDER + GAP + BORDER + gradient_with + BORDER,
                        -HEIGHT / 2.0 + BORDER,
                    ),
                uv: WHITE_UV,
                color: primary_color,
            });
            let secondary_color = self.secondary.color32();
            // Top right
            mesh.vertices.push(Vertex {
                pos: start_pos
                    - Vec2::new(
                        BORDER + GAP + BORDER + gradient_with + BORDER,
                        HEIGHT / 2.0 - BORDER,
                    ),
                uv: WHITE_UV,
                color: secondary_color,
            });
            // Bottom left
            mesh.vertices.push(Vertex {
                pos: start_pos
                    - Vec2::new(
                        color_width + BORDER + GAP + BORDER + gradient_with + BORDER,
                        -HEIGHT / 2.0 + BORDER,
                    ),
                uv: WHITE_UV,
                color: secondary_color,
            });
            // Bottom right
            mesh.vertices.push(Vertex {
                pos: start_pos
                    - Vec2::new(
                        BORDER + GAP + BORDER + gradient_with + BORDER,
                        -HEIGHT / 2.0 + BORDER,
                    ),
                uv: WHITE_UV,
                color: secondary_color,
            });
            // Gradient background
            mesh.add_colored_rect(
                Rect::from_min_max(
                    start_pos - Vec2::new(BORDER + gradient_with + BORDER, HEIGHT / 2.0),
                    start_pos - Vec2::new(BORDER, -HEIGHT / 2.0),
                ),
                Color32::BLACK,
            );
            // Gradient colors
            for (i, color) in self.gradient.into_iter().rev().enumerate() {
                mesh.add_colored_rect(
                    Rect::from_min_max(
                        start_pos
                            - Vec2::new(
                                width_per_gradient_color * (i + 1) as f32 + BORDER,
                                HEIGHT / 2.0 - BORDER,
                            ),
                        start_pos
                            - Vec2::new(
                                width_per_gradient_color * i as f32 + BORDER,
                                -HEIGHT / 2.0 + BORDER,
                            ),
                    ),
                    color.color32(),
                );
            }

            mesh
        };
        ui.painter().add(Shape::Mesh(Arc::new(mesh)));
    }
}

impl AssetUi for Scene {
    fn show(&self, ui: &mut egui::Ui, rect: egui::Rect) {
        show_pills(
            ui,
            rect.right_top() + Vec2::new(0.0, 1.0),
            self.selections()
                .into_iter()
                .map(|selection| (selection.mode.to_string(), Color32::GOLD))
                .collect(),
        );
    }
}