gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
use crate::settings::ClientSettings;
use crate::ui::animation_editor::AnimationUi;
use crate::ui::effect::EffectUi;
use crate::{
    app::client_timing::ClientTiming,
    pipeline::{
        renderer_callback::RendererCallback,
        thumbnails::{ThumbnailTextures, copy_thumbnail_to_clipboard},
        ui_gpu,
    },
    ui::{
        asset::CollectionsChangeButton,
        asset_tree::{AssetTree, TREE_WIDTH, TreeSelection},
        effect::widget::EffectWidget,
        window_common::{gled_window_frame, raise, tool_window_builder},
    },
    wgpu_render_state,
};
use egui::{
    Color32, Context, Id, Margin, ScrollArea, Stroke, UiKind, Vec2, ViewportId,
    scroll_area::ScrollBarVisibility::AlwaysVisible,
};
use gled_gpu::{
    effect_state::{EffectKey, EffectPipelines},
    scene_render::{EvalContext, ScenePaint, prepare_effect},
    shader_code::effect_shader_code,
};
use gled_model::{
    asset::{Asset, animation::Animation, scene::effect::Effect},
    collection::Collections,
    patching::{Patch, layout::LayoutSelection},
};
use gled_protocol::action::UiAction;
use gled_protocol::state::SoundSnapshot;
use naga::{
    front::wgsl::parse_str,
    valid::{Capabilities, ValidationFlags, Validator},
};
use std::sync::Arc;
use uuid::Uuid;
use wgpu::CommandEncoderDescriptor;

/// The one effect the animation editor previews.
const PREVIEW: EffectKey = (Uuid::nil(), 0);

#[derive(Debug, Default)]
pub(crate) struct AnimationWindow {
    open: bool,
    /// Set while the window still has to be brought forward.
    raise: bool,
    dirty: bool,
    tree: AssetTree<Animation>,
    effect: Option<Effect>,
    /// GPU state of the preview effect, under `PREVIEW`.
    pipelines: EffectPipelines,
    /// egui texture of its thumbnail.
    textures: ThumbnailTextures,
    /// The edited animation, rendered in place of the saved one.
    overwrite: Option<Arc<Asset<Animation>>>,
    error: Option<String>,
    preview: bool,
    copy_rendered_image_to_clipboard: bool,
}

impl AnimationWindow {
    fn validate(&mut self, collections: &Collections) {
        if let TreeSelection::Asset(animation) = &self.tree.selected()
            && let Some(effect) = self.effect.as_mut()
        {
            self.overwrite = Some(Arc::new(animation.clone()));
            let shader_code = effect_shader_code(effect, collections, self.overwrite.as_ref());
            let mut validator = Validator::new(ValidationFlags::all(), Capabilities::all());
            match parse_str(&shader_code.fragment_render_matrix)
                .map_err(|err| err.emit_to_string(&shader_code.fragment_render_matrix))
                .and_then(|module| {
                    validator
                        .validate(&module)
                        .map_err(|err| err.emit_to_string(&shader_code.fragment_render_matrix))
                }) {
                Ok(_) => {
                    self.pipelines
                        .state_mut(PREVIEW)
                        .set_matrix_shader_code(&ui_gpu(), &shader_code);
                    self.error.take();
                }
                Err(err) => {
                    self.error = Some(err);
                }
            }
        }
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    pub(crate) fn update(
        &mut self,
        ctx: &Context,
        timing: &ClientTiming,
        collections: &mut Collections,
        settings: &mut ClientSettings,
        sound_data: &SoundSnapshot,
    ) {
        if !self.open {
            self.dirty = false;
            return;
        }

        let mut validate = false;

        if let TreeSelection::Asset(..) = &self.tree.selected()
            && self.effect.is_none()
        {
            self.effect = Some(Default::default());
            validate = true;
        }

        if let Some(effect) = &mut self.effect {
            let state = self.pipelines.state_mut(PREVIEW);
            state.beat_progression = timing.beat_progression();
            state.beats_per_minute = timing.beats_per_minute();
            state.framerate = timing.framerate().unwrap_or_default();
            let preview_palette = settings
                .preview_palette()
                .and_then(|id| Asset::get(id, collections));
            let wgpu_render_state = wgpu_render_state();
            let device = wgpu_render_state.device;
            let queue = &wgpu_render_state.queue;
            prepare_effect(
                effect,
                state,
                queue,
                ScenePaint {
                    palette: preview_palette.as_ref().map(|palette| &palette.data),
                    selection: LayoutSelection::default(),
                    opacity: 1.0,
                },
                EvalContext {
                    // Only the thumbnail renders here, which needs no lamps.
                    patch: &Patch::default(),
                    collections,
                    sound: sound_data,
                },
            );
            let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
                label: Some("Render animations for animation editor"),
            });
            state.render(&mut encoder, true, false);
            if self.copy_rendered_image_to_clipboard {
                // Rendered right away, so the copy holds this frame.
                self.copy_rendered_image_to_clipboard = false;
                queue.submit([encoder.finish()]);
                copy_thumbnail_to_clipboard(ctx, &self.pipelines, &PREVIEW);
            } else {
                RendererCallback::add(encoder.finish());
            }
            self.textures.sync(&self.pipelines);
        }

        if self.preview {
            self.show_preview_window(ctx, collections, settings, sound_data);
        }

        ctx.show_viewport_immediate(
            ViewportId(Id::new("animations window")),
            tool_window_builder("Animations")
                .with_inner_size(Vec2::new(1000.0, 540.0))
                .with_min_inner_size(Vec2::new(1000.0, 540.0)),
            |ctx, _viewport_class| {
                raise(ctx, &mut self.raise);
                ctx.input(|input| {
                    if input.viewport().close_requested()
                        || crate::ui::window_common::embedded_close_clicked("Animations")
                    {
                        self.open = false;
                    }
                });

                gled_window_frame(ctx, "Animations", |ui| {
                    egui::Panel::left("animations tree")
                        .exact_size(TREE_WIDTH)
                        .resizable(false)
                        .show(ui, |ui| {
                            if self.tree.show(
                                ui,
                                ui.make_persistent_id("animations_tree"),
                                collections,
                            ) {
                                self.dirty = false;
                                validate = true;
                            }
                        });

                    egui::Panel::right("animation editor")
                        .exact_size(300.0)
                        .resizable(false)
                        .show(ui, |ui| {
                            if let TreeSelection::Asset(animation) = &mut self.tree.selected() {
                                ui.vertical_centered_justified(|ui| {
                                    if self.preview {
                                        if ui.button("👁 Close Preview").clicked() {
                                            self.preview = false;
                                        }
                                    } else if ui.button("👁 Open Preview").clicked() {
                                        self.preview = true;
                                    }
                                });
                                if animation.data.change_arguments_ui(ui) {
                                    validate = true;
                                    self.dirty = true;
                                }
                            }
                        });

                    egui::CentralPanel::default().show(ui, |ui| {
                        if self.tree.common_settings(ui, &mut self.dirty, collections) {
                            validate = true;
                            if let TreeSelection::Asset(animation) = &self.tree.selected() {
                                UiAction::ReloadShaderCode(Some(animation.id)).enqueue();
                            }
                        }

                        if let TreeSelection::Asset(animation) = &mut self.tree.selected() {
                            if let Some(mut error) = self.error.as_deref() {
                                let lines = error.lines().count().max(1);
                                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("Error compiling shader code:");
                                        ui.add(
                                            egui::TextEdit::multiline(&mut error)
                                                .font(egui::TextStyle::Monospace)
                                                .code_editor()
                                                .desired_rows(lines)
                                                .desired_width(f32::INFINITY),
                                        )
                                        .context_menu(
                                            |ui| {
                                                if ui.button("🖹 Copy error").clicked() {
                                                    ui.ctx().copy_text(error.to_owned());
                                                    ui.close_kind(UiKind::Menu);
                                                }
                                            },
                                        );
                                    });
                            }

                            if animation.data.change_shader_code_ui(ui) {
                                validate = true;
                                self.dirty = true;
                            }
                        }
                    });
                });
            },
        );

        if validate {
            self.validate(collections);
        }
    }

    pub(crate) fn open(&mut self) {
        self.open = true;
        self.raise = true;
    }

    pub(crate) fn show_preview_window(
        &mut self,
        ctx: &Context,
        collections: &mut Collections,
        settings: &mut ClientSettings,
        sound_data: &SoundSnapshot,
    ) {
        ctx.show_viewport_immediate(
            ViewportId(Id::new("animation preview window")),
            tool_window_builder("Animation Preview")
                .with_inner_size(Vec2::new(630.0, 440.0))
                .with_min_inner_size(Vec2::new(630.0, 440.0)),
            |ctx, _viewport_class| {
                ctx.input(|input| {
                    if input.viewport().close_requested()
                        || crate::ui::window_common::embedded_close_clicked("Animation Preview")
                    {
                        self.preview = false;
                    }
                });

                gled_window_frame(ctx, "Animation Preview", |ui| {
                    if let Some(effect) = &mut self.effect {
                        egui::Panel::right("animation preview right side")
                            .exact_size(300.0)
                            .resizable(false)
                            .show(ui, |ui| {
                                ScrollArea::vertical()
                                    .scroll_bar_visibility(AlwaysVisible)
                                    .max_height(ui.available_height())
                                    .show(ui, |ui| {
                                        effect.config_ui(
                                            ui,
                                            self.textures.texture_id(&PREVIEW),
                                            self.overwrite.as_ref(),
                                            false,
                                            &Patch::default(),
                                            1.0,
                                            collections,
                                            sound_data,
                                        )
                                    });
                            });
                    }

                    egui::CentralPanel::default().show(ui, |ui| {
                        egui::Frame::NONE
                            .inner_margin(Margin::from(6.0))
                            .stroke(Stroke::new(1.0, Color32::DARK_GRAY))
                            .show(ui, |ui| {
                                ui.label("Preview Palette");
                                ui.vertical_centered_justified(|ui| {
                                    if settings
                                        .preview_palette_mut()
                                        .collections_change_button(ui, collections)
                                    {
                                    }
                                });
                            });

                        ui.add_space(4.0);

                        if let Some(effect) = &mut self.effect {
                            egui::Frame::default()
                                .outer_margin(Margin::same(4))
                                .show(ui, |ui| {
                                    ui.add_sized(
                                        Vec2::splat(300.0),
                                        EffectWidget {
                                            show_group: false,
                                            selectable: None,
                                            effect,
                                            thumbnail: self.textures.texture_id(&PREVIEW),
                                            patch: None,
                                            beat_progression: None,
                                            collections,
                                            sound_data,
                                        },
                                    );
                                    if ui
                                        .vertical_centered_justified(|ui| {
                                            ui.button("🖹 Copy Image to Clipboard")
                                        })
                                        .inner
                                        .clicked()
                                    {
                                        self.copy_rendered_image_to_clipboard = true;
                                    }
                                });
                        }
                    });
                });
            },
        );
    }
}