use crate::ui::{effect::outline_if_touched, 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},
},
midi_controller::MidiEffectParameter,
},
collection::Collections,
};
use gled_protocol::state::SoundSnapshot;
use strum::IntoEnumIterator;
pub(crate) trait AnimationUi {
fn change_shader_code_ui(&mut self, ui: &mut Ui) -> bool;
fn change_arguments_ui(&mut self, ui: &mut Ui) -> bool;
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) .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;
};
let response = ui.vertical_centered_justified(|ui| {
changed |= value.percentage().change_button(
ui,
beat_progression,
collection,
sound_data,
);
});
outline_setting_if_touched(ui, count.f32, response.response.rect);
}
ArgumentKind::Degrees => {
let Some(value) = config.float(count.f32) else {
return false;
};
let response = ui.vertical_centered_justified(|ui| {
changed |=
value
.degrees()
.change_button(ui, beat_progression, collection, sound_data);
});
outline_setting_if_touched(ui, count.f32, response.response.rect);
}
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
}
}
fn outline_setting_if_touched(ui: &Ui, index: usize, rect: egui::Rect) {
if let Some(parameter) = u8::try_from(index)
.ok()
.and_then(MidiEffectParameter::setting)
{
outline_if_touched(ui, parameter, rect);
}
}