use crate::{
audio::sound_trigger_editor::{sound_trigger_editor, sound_trigger_minified},
ui::{asset_tree::AssetTree, curve_editor::CurveEditor, gled_slider::GledSlider},
};
use egui::{
Button, Color32, RichText, Sense, Stroke, UiBuilder, UiKind,
containers::menu::{MenuButton, MenuConfig},
};
use egui_ltreeview::TreeViewState;
use egui_phosphor_icons::icons;
use emath::{Pos2, Rect, Vec2, vec2};
use gled_model::{
asset::{
Asset, AssetTrait,
curve::{
Curve,
multiplied_curve::{MultipliedCurve, RangeDegrees, RangePercentage},
},
},
asset_id::AssetId,
collection::Collections,
sound::trigger_ref::SoundTriggerRef,
};
use gled_protocol::state::SoundSnapshot;
use std::f32::consts::TAU;
pub(crate) trait MultipliedCurveUi {
fn change_button(
&mut self,
ui: &mut egui::Ui,
beat_progression: f32,
collections: &mut Collections,
sound_data: &SoundSnapshot,
) -> bool;
}
impl MultipliedCurveUi for MultipliedCurve<RangePercentage> {
fn change_button(
&mut self,
ui: &mut egui::Ui,
beat_progression: f32,
collections: &mut Collections,
sound_data: &SoundSnapshot,
) -> bool {
let mut changed = false;
let mut rect = ui.available_rect_before_wrap();
rect.set_height(20.0);
let (left, right) = rect.split_left_right_at_fraction(0.5);
ui.horizontal(|ui| {
let preview_value = self.curve_preview_value(beat_progression, collections)
* self.multiplier
* self.sound_trigger_value(sound_data);
changed |= ui
.put(
left,
GledSlider::new(&mut self.multiplier, 1.0)
.horizontal()
.preview_value(preview_value)
.size(left.height()),
)
.changed();
changed |= shared_curve_and_trigger_menus(
ui,
right,
&mut self.curve,
&mut self.sound_trigger,
beat_progression,
collections,
sound_data,
);
});
changed
}
}
impl MultipliedCurveUi for MultipliedCurve<RangeDegrees> {
fn change_button(
&mut self,
ui: &mut egui::Ui,
beat_progression: f32,
collections: &mut Collections,
sound_data: &SoundSnapshot,
) -> bool {
let mut changed = false;
let mut rect = ui.available_rect_before_wrap();
rect.set_height(40.0);
let (left, right) = rect.split_left_right_at_fraction(0.5);
ui.horizontal(|ui| {
let preview_value = self.curve_preview_value(beat_progression, collections)
* self.multiplier
* self.sound_trigger_value(sound_data);
changed |= angle_dial(ui, left, &mut self.multiplier, preview_value);
changed |= shared_curve_and_trigger_menus(
ui,
right,
&mut self.curve,
&mut self.sound_trigger,
beat_progression,
collections,
sound_data,
);
});
changed
}
}
fn shared_curve_and_trigger_menus(
ui: &mut egui::Ui,
right: Rect,
curve_id: &mut Option<AssetId<Curve>>,
sound_trigger: &mut Option<SoundTriggerRef>,
beat_progression: f32,
collections: &mut Collections,
sound_data: &SoundSnapshot,
) -> bool {
let mut changed = false;
let (mut curve_rect, mut sound_trigger_rect) = right
.shrink2(vec2(2.0, 0.0))
.split_left_right_at_fraction(0.5);
curve_rect = curve_rect.shrink2(vec2(2.0, 0.0));
sound_trigger_rect = sound_trigger_rect.shrink2(vec2(2.0, 0.0));
ui.scope_builder(UiBuilder::default().max_rect(curve_rect), |ui| {
ui.take_available_space();
let rect = MenuButton::from_button(
Button::new(if curve_id.is_some() {
RichText::default()
} else {
icons::CHART_LINE.regular()
})
.min_size(sound_trigger_rect.size()),
)
.config(MenuConfig::new().close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside))
.ui(ui, |ui| {
ui.set_min_width(300.0);
if curve_id.is_some()
&& ui
.vertical_centered_justified(|ui| {
ui.add(Button::new("Remove Curve").fill(Color32::DARK_RED))
})
.inner
.clicked()
{
*curve_id = None;
changed = true;
ui.close_kind(UiKind::Menu);
};
ui.set_min_height(400.0);
if let Some(id) =
AssetTree::show_asset_selection(ui, ui.make_persistent_id(Curve::NAME), collections)
{
*curve_id = Some(id);
ui.data_mut(|d| {
d.remove::<TreeViewState<usize>>(ui.make_persistent_id(Curve::NAME))
});
ui.close_kind(UiKind::Menu);
changed = true;
}
})
.0
.rect;
if let Some(curve) = curve_id.and_then(|id| Asset::get(id, collections)) {
curve
.data
.clone()
.draw_curve(ui, false, Some(beat_progression), rect);
}
});
ui.scope_builder(UiBuilder::default().max_rect(sound_trigger_rect), |ui| {
ui.take_available_space();
let rect = MenuButton::from_button(
Button::new(if sound_trigger.is_some() {
RichText::default()
} else {
icons::MICROPHONE.regular()
})
.min_size(sound_trigger_rect.size()),
)
.config(MenuConfig::new().close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside))
.ui(ui, |ui| {
ui.set_min_width(300.0);
if sound_trigger.is_some()
&& ui
.vertical_centered_justified(|ui| {
ui.add(Button::new("Remove Trigger").fill(Color32::DARK_RED))
})
.inner
.clicked()
{
*sound_trigger = None;
changed = true;
ui.close_kind(UiKind::Menu);
return;
};
ui.set_min_height(400.0);
if sound_trigger.is_none() {
*sound_trigger = Some(SoundTriggerRef::default());
changed = true;
}
if let Some(trigger) = sound_trigger {
changed |= sound_trigger_editor(ui, trigger, sound_data);
}
})
.0
.rect;
if let Some(trigger) = sound_trigger {
sound_trigger_minified(ui, rect, trigger, sound_data);
}
});
changed
}
fn angle_dial(ui: &mut egui::Ui, area: Rect, value: &mut f32, preview_value: f32) -> bool {
const TOP_COLOR: Color32 = Color32::from_rgb(53, 53, 49);
const RING_COLOR: Color32 = Color32::from_rgb(103, 103, 94);
const HANDLE_COLOR: Color32 = Color32::from_rgb(59, 255, 0);
const PREVIEW_COLOR: Color32 = Color32::from_rgb(255, 176, 100);
const SNAP_RADIUS_FRACTION: f32 = 0.85;
let side = area.width().min(area.height());
let center = area.center();
let dial_rect = Rect::from_center_size(center, Vec2::splat(side));
let radius = side * 0.5;
let response = ui.allocate_rect(dial_rect, Sense::click_and_drag());
let mut changed = false;
if let Some(pointer) = response.interact_pointer_pos()
&& (response.dragged() || response.clicked())
{
let delta = pointer - center;
let r_norm = (delta.length() / radius).clamp(0.0, 1.0);
let mut angle = delta.y.atan2(delta.x) + std::f32::consts::FRAC_PI_2;
if angle < 0.0 {
angle += TAU;
}
let mut new_value = angle / TAU;
if r_norm > SNAP_RADIUS_FRACTION {
new_value = (new_value * 4.0).round() / 4.0;
if new_value >= 1.0 {
new_value = 0.0;
}
}
new_value = new_value.clamp(0.0, 1.0);
if (new_value - *value).abs() > f32::EPSILON {
*value = new_value;
changed = true;
}
}
let painter = ui.painter_at(dial_rect);
painter.circle_filled(center, radius, TOP_COLOR);
painter.circle_stroke(center, radius, Stroke::new(1.0, RING_COLOR));
for i in 0..4 {
let a = (i as f32) * std::f32::consts::FRAC_PI_2 - std::f32::consts::FRAC_PI_2;
let inner = center + Vec2::new(a.cos(), a.sin()) * (radius * 0.8);
let outer = center + Vec2::new(a.cos(), a.sin()) * radius;
painter.line_segment([inner, outer], Stroke::new(1.0, RING_COLOR));
}
let draw_hand = |val: f32, color: Color32, width: f32| {
let angle = val * TAU - std::f32::consts::FRAC_PI_2;
let tip = center + Vec2::new(angle.cos(), angle.sin()) * (radius - 1.0);
painter.line_segment([center, tip], Stroke::new(width, color));
};
if (preview_value - *value).abs() > f32::EPSILON {
draw_hand(preview_value, PREVIEW_COLOR, 1.5);
}
draw_hand(*value, HANDLE_COLOR, 2.0);
let label = format!("{:.0}°", *value * 360.0);
painter.text(
Pos2::new(center.x, area.max.y - 1.0),
egui::Align2::CENTER_BOTTOM,
label,
egui::FontId::proportional(radius.min(10.0)),
HANDLE_COLOR,
);
changed
}