tmaze 1.17.1

Simple multiplatform maze solving game for terminal written entirely in Rust
Documentation
pub mod track;

use menu::OptionDef;
use rodio::{OutputStream, OutputStreamHandle, Sink};

use crate::{
    app::{app::AppData, Activity},
    settings::Settings,
    ui::{menu, MenuItem, SliderDef},
};

use self::track::Track;

struct SoundHandles {
    _stream: OutputStream,
    handle: OutputStreamHandle,
    sink: Sink,
}

pub struct SoundPlayer {
    handles: Option<SoundHandles>,
    settings: Settings,
}

impl SoundPlayer {
    pub fn new(settings: Settings) -> Self {
        let Ok((stream, handle)) = rodio::OutputStream::try_default() else {
            log::warn!("Failed to create audio stream, no sound will be played");
            return Self {
                handles: None,
                settings,
            };
        };

        let sink = Sink::try_new(&handle).expect("Failed to create sink");

        Self {
            handles: Some(SoundHandles {
                _stream: stream,
                handle,
                sink,
            }),
            settings,
        }
    }

    #[inline]
    fn apply(&self, f: impl FnOnce(&Sink)) {
        if let Some(handles) = &self.handles {
            f(&handles.sink);
        }
    }

    #[allow(dead_code)]
    pub fn enqueue(&self, track: Track) {
        self.apply(|sink| {
            sink.append(track);
            sink.play();
        });
    }

    pub fn play_track(&self, track: Track) {
        self.apply(|sink| {
            sink.stop();
            sink.append(track);
            sink.play();
        });
    }

    #[allow(dead_code)]
    pub fn play_sound(&self, track: Track) {
        let Some(handle) = self.handles.as_ref().map(|h| &h.handle) else {
            return;
        };
        let sink = Sink::try_new(handle).expect("Failed to create sink");
        sink.set_volume(self.settings.get_audio_volume());
        sink.append(track);
        sink.play();
        sink.detach();
    }

    #[allow(dead_code)]
    pub fn wait(&self) {
        self.apply(|sink| sink.sleep_until_end());
    }

    pub fn set_volume(&self, volume: f32) {
        self.apply(|sink| sink.set_volume(volume));
    }
}

pub fn create_audio_settings(data: &mut AppData) -> Activity {
    fn update_vol(data: &mut AppData) {
        if data.settings.get_enable_audio() && data.settings.get_enable_music() {
            data.sound_player
                .set_volume(data.settings.get_audio_volume() * data.settings.get_music_volume());
        } else {
            data.sound_player.set_volume(0.0);
        }
    }

    let menu_config = menu::MenuConfig::new(
        "Audio settings",
        [
            MenuItem::Option(OptionDef {
                text: "Global mute".into(),
                val: !data.settings.get_enable_audio(),
                fun: Box::new(|mute, data| {
                    *mute = !*mute;
                    data.settings.set_enable_audio(!*mute);
                    update_vol(data);
                }),
            }),
            MenuItem::Slider(SliderDef {
                text: "Global volume".into(),
                val: (data.settings.get_audio_volume() * 5.0) as i32,
                range: 0..=5,
                as_num: false,
                fun: Box::new(|up, vol, data| {
                    *vol += if up { 1 } else { -1 };
                    data.settings.set_audio_volume(*vol as f32 / 5.0);
                    update_vol(data);
                }),
            }),
            MenuItem::Option(OptionDef {
                text: "Music mute".into(),
                val: !data.settings.get_enable_music(),
                fun: Box::new(|mute, data| {
                    *mute = !*mute;
                    data.settings.set_enable_music(!*mute);
                    update_vol(data);
                }),
            }),
            MenuItem::Slider(SliderDef {
                text: "Music volume".into(),
                val: (data.settings.get_music_volume() * 5.0) as i32,
                range: 0..=5,
                as_num: false,
                fun: Box::new(|up, vol, data| {
                    *vol += if up { 1 } else { -1 };
                    data.settings.set_music_volume(*vol as f32 / 5.0);
                    update_vol(data);
                }),
            }),
            MenuItem::Separator,
            MenuItem::Text("Exit".into()),
        ],
    );

    Activity::new_base_boxed("audio settings", menu::Menu::new(menu_config))
}