gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
use crate::ui::effect::EffectUi;
use crate::{pipeline::thumbnails::ThumbnailTextures, ui::effect::widget::EffectWidget};
use egui::{Button, Color32, Ui, Vec2};
use gled_model::{
    asset::scene::{Scene, effect::Effect},
    collection::Collections,
    patching::Patch,
};
use gled_protocol::state::SoundSnapshot;
use uuid::Uuid;

#[derive(Default, Debug)]
pub(crate) struct SceneEffectEditorState {
    pub(crate) selected_effect: usize,
    /// The selection as last sent to the engine or taken from it; a pick
    /// here differs from it until it is sent.
    synced_effect: usize,
}

impl SceneEffectEditorState {
    /// Takes over the engine's selected effect, which a MIDI controller may
    /// have changed. Call it only once the engine has applied everything
    /// sent, so it does not undo a pick on its way.
    pub(crate) fn follow_engine(&mut self, engine_effect: usize) {
        self.selected_effect = engine_effect;
        self.synced_effect = engine_effect;
    }

    /// The effect picked here since the last sync, which the engine has to
    /// select too.
    pub(crate) fn take_change(&mut self) -> Option<usize> {
        (self.selected_effect != self.synced_effect).then(|| {
            self.synced_effect = self.selected_effect;
            self.selected_effect
        })
    }

    /// Back to the first effect, as the engine does when another scene or
    /// project is selected, so nothing is sent.
    pub(crate) fn reset(&mut self) {
        self.selected_effect = 0;
        self.synced_effect = 0;
    }

    pub(crate) fn clamp(&mut self, effect_count: usize) {
        if effect_count == 0 {
            self.selected_effect = 0;
        } else {
            self.selected_effect = self.selected_effect.min(effect_count - 1);
        }
    }
}

/// `owner` and `textures` locate the thumbnails of the scene's effects.
pub(crate) fn selected_effect_editor_ui(
    ui: &mut Ui,
    scene: &mut Scene,
    owner: Uuid,
    textures: &ThumbnailTextures,
    state: &mut SceneEffectEditorState,
    allow_animation_change: bool,
    patch: &Patch,
    beat_progression: f32,
    collections: &mut Collections,
    sound_data: &SoundSnapshot,
) -> bool {
    state.clamp(scene.effects().len());

    let mut changed = false;
    if let Some(effect) = scene.effect(state.selected_effect) {
        changed |= effect.config_ui(
            ui,
            textures.texture_id(&(owner, state.selected_effect)),
            None,
            allow_animation_change,
            patch,
            beat_progression,
            collections,
            sound_data,
        );
    }

    ui.separator();

    ui.vertical_centered_justified(|ui| {
        if ui
            .add(Button::new("+ Add Animation").fill(Color32::DARK_GREEN))
            .clicked()
        {
            state.selected_effect = scene.add_effect(Effect::default());
            changed = true;
        }
    });
    ui.vertical_centered_justified(|ui| {
        if ui
            .add(Button::new("🗐 Duplicate Animation").fill(Color32::DARK_BLUE))
            .clicked()
            && let Some(effect) = scene.effect(state.selected_effect).cloned()
        {
            state.selected_effect = scene.add_effect(effect);
            changed = true;
        }
    });
    ui.vertical_centered_justified(|ui| {
        if ui
            .add(Button::new("🗑 Remove Animation").fill(Color32::DARK_RED))
            .clicked()
        {
            scene.remove_effect(state.selected_effect);
            state.selected_effect = state.selected_effect.saturating_sub(1);
            state.clamp(scene.effects().len());
            changed = true;
        }
    });

    changed
}

pub(crate) fn scene_effect_list_ui(
    ui: &mut Ui,
    scene: &Scene,
    owner: Uuid,
    textures: &ThumbnailTextures,
    state: &mut SceneEffectEditorState,
    show_group: bool,
    patch: Option<&Patch>,
    beat_progression: Option<f32>,
    collections: &mut Collections,
    sound_data: &SoundSnapshot,
) {
    state.clamp(scene.effects().len());

    ui.horizontal_wrapped(|ui| {
        for (index, effect) in scene.effects().iter().enumerate() {
            ui.add_sized(
                Vec2::splat(100.0),
                EffectWidget {
                    show_group,
                    selectable: Some((&mut state.selected_effect, index)),
                    effect,
                    thumbnail: textures.texture_id(&(owner, index)),
                    patch,
                    beat_progression,
                    collections,
                    sound_data,
                },
            );
        }
    });
}

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

    #[test]
    fn an_effect_picked_here_is_sent_once() {
        let mut state = SceneEffectEditorState {
            selected_effect: 2,
            ..Default::default()
        };
        assert_eq!(state.take_change(), Some(2));
        assert_eq!(state.take_change(), None);
    }

    #[test]
    fn going_back_to_the_first_effect_is_not_sent() {
        let mut state = SceneEffectEditorState {
            selected_effect: 2,
            ..Default::default()
        };
        let _ = state.take_change();
        state.reset();
        assert_eq!(state.take_change(), None);
    }

    #[test]
    fn an_effect_selected_in_the_engine_is_shown_and_not_sent_back() {
        let mut state = SceneEffectEditorState::default();
        state.follow_engine(3);
        assert_eq!(state.selected_effect, 3);
        assert_eq!(state.take_change(), None);
    }
}