alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! App lifecycle owner systems.

use crate::ecs::events::lifecycle::{LifecycleRequested, ShutdownRequest};
use bevy::{
    app::AppExit,
    prelude::{MessageReader, MessageWriter, ResMut, Resource},
};

/// Authoritative application lifecycle state.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Resource)]
pub(super) struct LifecycleState {
    /// Current lifecycle phase.
    phase: LifecyclePhase,
}

impl LifecycleState {
    /// Accepts the first shutdown request and preserves it as terminal state.
    const fn request_shutdown(&mut self, request: ShutdownRequest) {
        if matches!(self.phase, LifecyclePhase::Running) {
            self.phase = LifecyclePhase::ShuttingDown(request);
        }
    }

    /// Returns the accepted shutdown request, if shutdown has begun.
    const fn shutdown_request(self) -> Option<ShutdownRequest> {
        match self.phase {
            LifecyclePhase::Running => None,
            LifecyclePhase::ShuttingDown(request) => Some(request),
        }
    }
}

/// Lifecycle phase for the app boundary.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum LifecyclePhase {
    /// The app is accepting normal editor work.
    #[default]
    Running,
    /// Shutdown has been accepted and is now terminal.
    ShuttingDown(ShutdownRequest),
}

/// Applies lifecycle requests at the Bevy app boundary.
pub(super) fn apply_lifecycle_requests(
    mut requests: MessageReader<LifecycleRequested>,
    mut lifecycle: ResMut<LifecycleState>,
    mut app_exit: MessageWriter<AppExit>,
) {
    for request in requests.read() {
        match request {
            LifecycleRequested::Shutdown(shutdown) => {
                lifecycle.request_shutdown(*shutdown);
            }
        }
    }

    if let Some(request) = lifecycle.shutdown_request() {
        let _sent = app_exit.write(request.app_exit());
    }
}

#[cfg(test)]
mod tests {
    use super::{LifecyclePhase, LifecycleState};
    use crate::ecs::events::lifecycle::{ShutdownReason, ShutdownRequest};

    #[test]
    fn shutdown_state_is_terminal_after_first_request() {
        let mut state = LifecycleState::default();
        let request = ShutdownRequest::successful_user_command();

        state.request_shutdown(request);
        state.request_shutdown(request);

        assert_eq!(state.phase, LifecyclePhase::ShuttingDown(request));
        assert_eq!(state.shutdown_request(), Some(request));
        assert_eq!(request.reason(), ShutdownReason::UserCommand);
    }
}