mod render;
use std::time::Duration;
use bevy_app::{App, PostUpdate};
use bevy_ecs::change_detection::DetectChanges;
use bevy_ecs::prelude::{
Commands, Component, Entity, IntoScheduleConfigs, Local, Query, Res, ResMut, Resource, With,
};
use bevy_ui::UiSystems;
use plurimus::bui::ComputedNodeRect;
use plurimus::core::UiOrder;
use plurimus::core::ratatui_core::layout::Rect;
use plurimus::core::ratatui_core::style::Color;
use plurimus::ui::{ComputedWidgetArea, ModalOpen};
use serde::Deserialize;
use super::command::Outcome;
use super::journal;
use super::layout::HintRow;
use super::settings::Settings;
use super::theme::Theme;
pub fn plugin(app: &mut App) {
app.init_resource::<Cues>();
app.add_systems(
PostUpdate,
(dim_backdrop, cue_arrivals).after(UiSystems::PostLayout),
);
render::install(app);
}
const MOTION_KEY: [&str; 1] = ["motion"];
#[derive(Deserialize, Clone, Copy, PartialEq, Eq, Debug, Default)]
#[serde(rename_all = "lowercase")]
pub enum Motion {
#[default]
Full,
Reduced,
Off,
}
impl Motion {
const fn name(self) -> &'static str {
match self {
Self::Full => "full",
Self::Reduced => "reduced",
Self::Off => "off",
}
}
const fn next(self) -> Self {
match self {
Self::Full => Self::Reduced,
Self::Reduced => Self::Off,
Self::Off => Self::Full,
}
}
#[must_use]
pub const fn length(self, play: Play) -> Duration {
match (self, play) {
(Self::Off, _) | (Self::Reduced, Play::Coalesce) => Duration::ZERO,
(Self::Full | Self::Reduced, _) => play.length(),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
enum Key {
Backdrop,
Receipt,
Arrival,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Play {
Dim(Color),
Receipt(Color),
Coalesce,
}
impl Play {
const fn key(self) -> Key {
match self {
Self::Dim(_) => Key::Backdrop,
Self::Receipt(_) => Key::Receipt,
Self::Coalesce => Key::Arrival,
}
}
const fn length(self) -> Duration {
Duration::from_millis(match self {
Self::Dim(_) => 120,
Self::Receipt(_) => 400,
Self::Coalesce => 150,
})
}
const fn is_held(self) -> bool {
matches!(self, Self::Dim(_))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Cue {
Play {
play: Play,
area: Rect,
spared: Rect,
},
Stop(Key),
}
#[derive(Resource, Default, Debug)]
pub struct Cues(Vec<Cue>);
impl Cues {
pub fn play(&mut self, play: Play, area: Rect) {
self.0.push(Cue::Play {
play,
area,
spared: Rect::ZERO,
});
}
}
#[derive(Component, Debug)]
pub struct Arriving;
pub fn cycle(mut settings: ResMut<Settings>) -> Outcome {
let motion = settings.motion.next();
settings.motion = motion;
match settings.keep(&MOTION_KEY, motion.name()) {
Ok(()) => journal::say(format!("motion {}", motion.name())),
Err(error) => journal::warn(format!(
"motion {}, for this session only: {error}",
motion.name()
)),
}
Outcome::Done
}
fn dim_backdrop(
overlays: Query<(&ComputedWidgetArea, Option<&UiOrder>), With<ModalOpen>>,
hint_rows: Query<&ComputedNodeRect, With<HintRow>>,
theme: Res<Theme>,
mut cues: ResMut<Cues>,
mut dimmed: Local<Option<(Rect, Rect)>>,
) {
let topmost = overlays
.iter()
.filter(|(area, _)| !area.0.is_empty())
.max_by_key(|(_, order)| order.map_or(i32::MIN, |order| order.0))
.map(|(area, _)| area.0);
let spared = hint_rows.single().map_or(Rect::ZERO, |row| row.rect);
let standing = topmost.map(|area| (area, spared));
if standing == *dimmed && !theme.is_changed() {
return;
}
*dimmed = standing;
match standing {
Some((area, spared)) => cues.0.push(Cue::Play {
play: Play::Dim(theme.dim),
area,
spared,
}),
None => cues.0.push(Cue::Stop(Key::Backdrop)),
}
}
fn cue_arrivals(
arriving: Query<(Entity, &ComputedWidgetArea), With<Arriving>>,
mut cues: ResMut<Cues>,
mut commands: Commands,
) {
for (entity, area) in &arriving {
if area.0.is_empty() {
continue;
}
cues.play(Play::Coalesce, area.0);
commands.entity(entity).remove::<Arriving>();
}
}
#[cfg(test)]
mod tests;