use bevy::{audio::Volume, prelude::*, ui::Val::*};
#[cfg(feature = "hot_patch")]
use bevy_simple_subsecond_system::hot;
use crate::{
audio::{DEFAULT_VOLUME, max_volume},
screens::Screen,
theme::prelude::*,
};
pub(super) fn plugin(app: &mut App) {
app.init_resource::<VolumeSliderSettings>();
app.add_systems(OnEnter(Screen::Settings), spawn_settings_screen);
app.register_type::<GlobalVolumeLabel>();
app.add_systems(
Update,
(
update_global_volume.run_if(resource_exists_and_changed::<VolumeSliderSettings>),
update_volume_label,
)
.run_if(in_state(Screen::Settings)),
);
}
#[cfg_attr(feature = "hot_patch", hot)]
fn spawn_settings_screen(mut commands: Commands) {
commands.spawn((
widget::ui_root("Settings Screen"),
StateScoped(Screen::Settings),
children![
widget::header("Settings"),
(
Name::new("Settings Grid"),
Node {
display: Display::Grid,
row_gap: Px(10.0),
column_gap: Px(30.0),
grid_template_columns: RepeatedGridTrack::px(2, 400.0),
..default()
},
children![
(
widget::label("Audio Volume"),
Node {
justify_self: JustifySelf::End,
..default()
}
),
volume_widget(),
],
),
widget::button("Back", enter_title_screen),
],
));
}
fn volume_widget() -> impl Bundle {
(
Node {
justify_self: JustifySelf::Start,
..default()
},
children![
widget::button_small("-", lower_volume),
widget::button_small("+", raise_volume),
(
Node {
padding: UiRect::horizontal(Px(10.0)),
justify_content: JustifyContent::Center,
..default()
},
children![(widget::label(""), GlobalVolumeLabel)],
),
],
)
}
#[derive(Resource, Reflect, Debug)]
struct VolumeSliderSettings(usize);
impl VolumeSliderSettings {
fn increment(&mut self) {
self.0 = Self::MAX_TICK_COUNT.min(self.0 + 1);
}
fn decrement(&mut self) {
self.0 = self.0.saturating_sub(1);
}
fn volume(&self) -> Volume {
let max_gain = max_volume().to_linear();
let mid_gain = DEFAULT_VOLUME.to_linear();
let t = self.0 as f32 / Self::MAX_TICK_COUNT as f32;
let gain = Self::curved_interpolation(t, mid_gain, max_gain);
Volume::Linear(gain)
}
fn curved_interpolation(t: f32, a: f32, b: f32) -> f32 {
if t <= 0.5 {
let t2 = t / 0.5;
a * (3.0 * t2.powi(2) - 2.0 * t2.powi(3))
} else {
let t2 = (t - 0.5) / 0.5;
let smooth = 3.0 * t2.powi(2) - 2.0 * t2.powi(3);
a + (b - a) * smooth
}
}
const MAX_TICK_COUNT: usize = 20;
}
impl Default for VolumeSliderSettings {
fn default() -> Self {
Self(Self::MAX_TICK_COUNT / 2)
}
}
#[cfg_attr(feature = "hot_patch", hot)]
fn update_global_volume(
mut global_volume: ResMut<GlobalVolume>,
volume_step: Res<VolumeSliderSettings>,
) {
global_volume.volume = volume_step.volume();
}
#[cfg_attr(feature = "hot_patch", hot)]
fn lower_volume(_trigger: Trigger<Pointer<Click>>, mut volume_step: ResMut<VolumeSliderSettings>) {
volume_step.decrement();
}
#[cfg_attr(feature = "hot_patch", hot)]
fn raise_volume(_trigger: Trigger<Pointer<Click>>, mut volume_step: ResMut<VolumeSliderSettings>) {
volume_step.increment();
}
#[derive(Component, Reflect)]
#[reflect(Component)]
struct GlobalVolumeLabel;
#[cfg_attr(feature = "hot_patch", hot)]
fn update_volume_label(
mut label: Single<&mut Text, With<GlobalVolumeLabel>>,
slider: Res<VolumeSliderSettings>,
) {
let ticks = slider.0;
let filled = "█".repeat(ticks);
let empty = " ".repeat(VolumeSliderSettings::MAX_TICK_COUNT - ticks);
let text = filled + &empty + "|";
label.0 = text;
}
#[cfg_attr(feature = "hot_patch", hot)]
fn enter_title_screen(
_trigger: Trigger<Pointer<Click>>,
mut next_screen: ResMut<NextState<Screen>>,
) {
next_screen.set(Screen::Title);
}