gled-ui 2.30.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Editors for animations: the shader code, the argument list, and the
//! argument values of an effect.

use crate::ui::multiplied_curve::MultipliedCurveUi;
use egui::{
    Button, Color32, ComboBox, CursorIcon, Image, Layout, Margin, Sense, Stroke, TextureHandle,
    TextureId, Ui, UiKind, Vec2, load::SizedTexture,
    scroll_area::ScrollBarVisibility::AlwaysVisible,
};
use egui_extras::syntax_highlighting::CodeTheme;
use gled_gpu::shader_code::animation_shader_code;
use gled_model::{
    asset::animation::{
        Animation,
        argument::{Argument, ArgumentKind, ArgumentKindId, variables::VariablesCount},
        config::{AnimationConfig, float_value::FloatValue},
    },
    collection::Collections,
};
use gled_protocol::state::SoundSnapshot;
use strum::IntoEnumIterator;

pub(crate) trait AnimationUi {
    /// The code editor. Returns whether the code changed.
    fn change_shader_code_ui(&mut self, ui: &mut Ui) -> bool;
    /// The list of arguments. Returns whether one changed.
    fn change_arguments_ui(&mut self, ui: &mut Ui) -> bool;
    /// The argument values of an effect using this animation. `rendered` is
    /// the effect's thumbnail, used to pick a center.
    fn config_ui(
        &self,
        config: &mut AnimationConfig,
        ui: &mut Ui,
        rendered: TextureId,
        svg: Option<egui::TextureHandle>,
        beat_progression: f32,
        collections: &mut Collections,
        sound_data: &SoundSnapshot,
    ) -> bool;
}

impl AnimationUi for Animation {
    fn change_shader_code_ui(&mut self, ui: &mut Ui) -> bool {
        let mut changed = false;

        let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
            let mut layout_job = egui_extras::syntax_highlighting::highlight(
                ui.ctx(),
                ui.style(),
                &CodeTheme::default(),
                buf.as_str(),
                "rust",
            );
            layout_job.wrap.max_width = wrap_width;
            ui.fonts_mut(|f| f.layout_job(layout_job))
        };

        ui.with_layout(
            egui::Layout::left_to_right(egui::Align::Min).with_cross_justify(true),
            |ui| {
                egui::ScrollArea::vertical()
                    .scroll_bar_visibility(AlwaysVisible)
                    .show(ui, |ui| {
                        let response = ui.add(
                            egui::TextEdit::multiline(&mut self.shader_code)
                                .id_salt("Code Editor")
                                .font(egui::TextStyle::Monospace) // for cursor height
                                .code_editor()
                                .desired_rows(20)
                                .lock_focus(true)
                                .desired_width(f32::INFINITY)
                                .layouter(&mut layouter),
                        );
                        response.context_menu(|ui| {
                            if ui.button("🖹 Copy whole source code").clicked() {
                                ui.ctx()
                                    .copy_text(animation_shader_code(self).fragment_render_matrix);
                                ui.close_kind(UiKind::Menu);
                            }
                        });
                        changed = response.changed();
                    });
            },
        );

        changed
    }

    fn change_arguments_ui(&mut self, ui: &mut Ui) -> bool {
        let mut changed = false;
        let mut remove = None;

        let mut count = VariablesCount::default();
        for argument in self.arguments.iter() {
            count += argument.kind.variables().count();
        }
        if !count.possible() {
            ui.vertical_centered_justified(|ui| {
                egui::Frame::NONE
                    .inner_margin(Margin::from(3.0))
                    .stroke(Stroke::new(2.0, Color32::RED))
                    .fill(Color32::DARK_RED)
                    .show(ui, |ui| {
                        ui.heading("Too many variables");
                        ui.label(format!("u32: {}/{}", count.u32, 3));
                        ui.label(format!("f32: {}/{}", count.f32, 7));
                    });
            });
        }

        ui.vertical_centered_justified(|ui| {
            if ui.button("+ Add argument").clicked() {
                self.arguments.push(Argument::default());
                changed = true;
            }
        });

        egui::ScrollArea::vertical()
            .scroll_bar_visibility(AlwaysVisible)
            .show(ui, |ui| {
                for (index, argument) in self.arguments.iter_mut().enumerate() {
                    count += argument.kind.variables().count();
                    egui::Frame::NONE
                        .inner_margin(Margin::from(3.0))
                        .stroke(Stroke::new(1.0, Color32::DARK_GRAY))
                        .show(ui, |ui| {
                            changed |= argument.change_ui(ui, index, &mut remove);
                        });
                }
            });
        if let Some(index) = remove {
            self.arguments.remove(index);
            changed = true;
        }

        changed
    }

    fn config_ui(
        &self,
        config: &mut AnimationConfig,
        ui: &mut Ui,
        rendered: TextureId,
        svg: Option<egui::TextureHandle>,
        beat_progression: f32,
        collections: &mut Collections,
        sound_data: &SoundSnapshot,
    ) -> bool {
        let mut changed = false;
        let mut count = VariablesCount::default();
        for argument in self.arguments.iter() {
            changed |= argument.config_ui(
                config,
                ui,
                count,
                rendered,
                svg.clone(),
                beat_progression,
                collections,
                sound_data,
            );
            count += argument.kind.variables().count();
        }

        changed
    }
}

trait ArgumentUi {
    fn change_ui(&mut self, ui: &mut Ui, index: usize, remove: &mut Option<usize>) -> bool;
    fn config_ui(
        &self,
        config: &mut AnimationConfig,
        ui: &mut Ui,
        count: VariablesCount,
        rendered: TextureId,
        svg: Option<TextureHandle>,
        beat_progression: f32,
        collection: &mut Collections,
        sound_data: &SoundSnapshot,
    ) -> bool;
}

impl ArgumentUi for Argument {
    fn change_ui(&mut self, ui: &mut Ui, index: usize, remove: &mut Option<usize>) -> bool {
        let mut changed = false;

        ui.horizontal(|ui| {
            ui.label("Name");
            ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
                if ui
                    .add(Button::new("🗑 Remove Argument").fill(Color32::DARK_RED))
                    .clicked()
                {
                    *remove = Some(index);
                }
            });
        });
        ui.vertical_centered_justified(|ui| {
            changed |= ui.text_edit_singleline(&mut self.name).changed();
        });

        ui.label("Generated Function");
        ui.vertical_centered_justified(|ui| {
            if ui
                .add_enabled_ui(false, |ui| ui.text_edit_singleline(&mut self.function()))
                .inner
                .on_disabled_hover_text(format!("Click to copy \"{}()\"", self.function_name()))
                .clicked()
            {
                ui.ctx().copy_text(format!("{}()", self.function_name()));
            }
        });

        ui.label("Kind");
        ComboBox::from_id_salt(format!("AnimationKind:{index}"))
            .selected_text(ArgumentKindId::from(&self.kind).as_ref())
            .show_ui(ui, |ui| {
                let mut id = ArgumentKindId::from(&self.kind);
                for new_id in ArgumentKindId::iter() {
                    if ui
                        .selectable_value(&mut id, new_id, new_id.as_ref())
                        .clicked()
                    {
                        self.kind = ArgumentKind::from(new_id);
                        changed = true;
                    }
                }
            });

        match &mut self.kind {
            ArgumentKind::Center
            | ArgumentKind::Percentage
            | ArgumentKind::Degrees
            | ArgumentKind::Checkbox => (),
            ArgumentKind::Selection { variants } => {
                ui.label("Variants");
                ui.vertical_centered_justified(|ui| {
                    if ui.button("+ Add Variant").clicked() {
                        variants.push("New Variant".to_string());
                        changed = true;
                    }
                });
                let mut remove = None;
                for (index, variant) in variants.iter_mut().enumerate() {
                    ui.horizontal(|ui| {
                        if ui.add(Button::new("🗑").fill(Color32::DARK_RED)).clicked() {
                            remove = Some(index);
                        }
                        ui.label(format!("{index}"));
                        changed |= ui.text_edit_singleline(variant).changed();
                    });
                }
                if let Some(index) = remove {
                    variants.remove(index);
                    changed = true;
                }
            }
            ArgumentKind::Slider { min, max } => {
                ui.label("Min");
                ui.vertical_centered_justified(|ui| {
                    changed |= ui.add(egui::DragValue::new(min)).changed();
                });

                ui.label("Max");
                ui.vertical_centered_justified(|ui| {
                    changed |= ui.add(egui::DragValue::new(max)).changed();
                });
            }
        }

        changed
    }

    fn config_ui(
        &self,
        config: &mut AnimationConfig,
        ui: &mut Ui,
        count: VariablesCount,
        rendered: TextureId,
        svg: Option<TextureHandle>,
        beat_progression: f32,
        collection: &mut Collections,
        sound_data: &SoundSnapshot,
    ) -> bool {
        let mut changed = false;
        ui.label(&self.name);
        match &self.kind {
            ArgumentKind::Center => {
                ui.vertical_centered_justified(|ui| {
                    ui.menu_button("Select center of animation", |ui| {
                        let size = 300.0;
                        let res = ui.add(
                            Image::new(SizedTexture::new(rendered, Vec2::splat(size)))
                                .sense(Sense::click()),
                        );
                        if let Some(svg_texture_handle) = svg {
                            ui.put(res.rect, {
                                Image::new(SizedTexture::new(
                                    svg_texture_handle.id(),
                                    Vec2::splat(size),
                                ))
                            });
                        }

                        if let Some(pos) =
                            res.hover_pos().map(|pos| pos - res.rect.min).filter(|pos| {
                                pos.x > 0.0 || pos.y > 0.0 || pos.x < size || pos.y < size
                            })
                        {
                            ui.output_mut(|o| o.cursor_icon = CursorIcon::Crosshair);
                            if let Some(float) = config.float(count.f32) {
                                *float = FloatValue::F32(pos.x / size);
                            }
                            if let Some(float) = config.float(count.f32 + 1) {
                                *float = FloatValue::F32(1.0 - pos.y / size);
                            }
                            changed = true;
                        }
                        if res.clicked() {
                            ui.close_kind(UiKind::Menu);
                        }
                    });
                });
            }
            ArgumentKind::Selection { variants } => {
                let Some(value) = config.u32(count.u32) else {
                    return false;
                };
                ui.horizontal(|ui| {
                    for (i, variant) in variants.iter().enumerate() {
                        if ui.radio_value(value, i as u32, variant).changed() {
                            changed = true;
                        }
                    }
                });
            }
            ArgumentKind::Percentage => {
                let Some(value) = config.float(count.f32) else {
                    return false;
                };

                ui.vertical_centered_justified(|ui| {
                    changed |= value.percentage().change_button(
                        ui,
                        beat_progression,
                        collection,
                        sound_data,
                    );
                });
            }
            ArgumentKind::Degrees => {
                let Some(value) = config.float(count.f32) else {
                    return false;
                };

                ui.vertical_centered_justified(|ui| {
                    changed |=
                        value
                            .degrees()
                            .change_button(ui, beat_progression, collection, sound_data);
                });
            }
            ArgumentKind::Slider { min, max } => {
                let Some(value) = config.u32(count.u32) else {
                    return false;
                };

                ui.vertical_centered_justified(|ui| {
                    changed |= ui.add(egui::Slider::new(value, *min..=*max)).changed();
                });
            }
            ArgumentKind::Checkbox => {
                let Some(value) = config.u32(count.u32) else {
                    return false;
                };

                let mut bool_value = *value != 0;

                if ui.checkbox(&mut bool_value, "").changed() {
                    *value = bool_value as u32;
                    changed = true;
                }
            }
        }
        changed
    }
}