use bevy_app::{App, Update};
use bevy_ecs::change_detection::DetectChanges;
use bevy_ecs::prelude::{IntoScheduleConfigs, Res, ResMut, Resource, SystemSet};
pub(super) mod surface;
pub use surface::{FocusStop, ShownSurface, SurfaceRoot, shown_in, spawn_surface};
pub fn plugin(app: &mut App) {
app.init_resource::<ActivePage>();
app.init_resource::<LastShown>();
app.configure_sets(Update, (PageSystems::Turn, PageSystems::Show).chain());
app.add_systems(Update, remember_the_page.in_set(PageSystems::Turn));
app.add_systems(Update, surface::show_the_surface.in_set(PageSystems::Show));
}
#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum PageSystems {
Turn,
Show,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Page {
Overview,
Ledger,
Compare,
RothConversions,
SsaBenefits,
MonteCarlo,
Historical,
Accounts,
Income,
Expenses,
Cliffs,
Transfers,
Conversions,
Contributions,
Events,
People,
Residency,
Household,
Settings,
Market,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Group {
Tools,
Plan,
}
impl Group {
pub const ALL: [Self; 2] = [Self::Tools, Self::Plan];
pub const fn title(self) -> &'static str {
match self {
Self::Tools => "Tools",
Self::Plan => "Plan",
}
}
pub const fn first(self) -> Page {
match self {
Self::Tools => Page::RothConversions,
Self::Plan => Page::Accounts,
}
}
pub const fn tab(self) -> usize {
OWN_TABS + self as usize
}
const fn at(tab: usize) -> Self {
Self::ALL[tab - OWN_TABS]
}
}
impl Page {
pub const ALL: [Self; 20] = [
Self::Overview,
Self::Ledger,
Self::Compare,
Self::RothConversions,
Self::SsaBenefits,
Self::MonteCarlo,
Self::Historical,
Self::Accounts,
Self::Income,
Self::Expenses,
Self::Cliffs,
Self::Transfers,
Self::Conversions,
Self::Contributions,
Self::Events,
Self::People,
Self::Residency,
Self::Household,
Self::Settings,
Self::Market,
];
pub const fn title(self) -> &'static str {
match self {
Self::Overview => "Overview",
Self::Ledger => "Ledger",
Self::Compare => "Compare",
Self::RothConversions => "Roth Conversions",
Self::SsaBenefits => "SSA Benefits",
Self::MonteCarlo => "Monte Carlo",
Self::Historical => "Historical",
Self::Accounts => "Accounts",
Self::Income => "Income",
Self::Expenses => "Expenses",
Self::Cliffs => "Cliffs",
Self::Transfers => "Transfers",
Self::Conversions => "Conversions",
Self::Contributions => "Contributions",
Self::Events => "Events",
Self::People => "People",
Self::Residency => "Residency",
Self::Household => "Household",
Self::Settings => "Settings",
Self::Market => "Market",
}
}
pub const fn label(self) -> &'static str {
match self {
Self::Overview => "overview",
Self::Ledger => "ledger",
Self::Compare => "compare",
Self::RothConversions => "roth-conversions",
Self::SsaBenefits => "ssa-benefits",
Self::MonteCarlo => "monte-carlo",
Self::Historical => "historical",
Self::Accounts => "accounts",
Self::Income => "income",
Self::Expenses => "expenses",
Self::Cliffs => "cliffs",
Self::Transfers => "transfers",
Self::Conversions => "conversions",
Self::Contributions => "contributions",
Self::Events => "events",
Self::People => "people",
Self::Residency => "residency",
Self::Household => "household",
Self::Settings => "settings",
Self::Market => "market",
}
}
pub const fn doc(self) -> &'static str {
match self {
Self::Overview => "show the overview",
Self::Ledger => "show the year ledger",
Self::Compare => "compare the document with other files",
Self::RothConversions => "search Roth conversion ladders",
Self::SsaBenefits => "search Social Security claim ages",
Self::MonteCarlo => "run the plan through random markets",
Self::Historical => "run the plan from every historical start year",
Self::Accounts => "edit the accounts",
Self::Income => "edit the income sources",
Self::Expenses => "edit the expenses",
Self::Cliffs => "edit the MAGI cliffs",
Self::Transfers => "edit the transfers",
Self::Conversions => "edit the Roth conversions",
Self::Contributions => "edit the contributions",
Self::Events => "edit the named events",
Self::People => "edit the people",
Self::Residency => "edit the residency",
Self::Household => "edit filing status and medicare",
Self::Settings => "edit the plan settings",
Self::Market => "edit the market assumptions",
}
}
pub const fn heading(self) -> Option<&'static str> {
if let Some(group) = self.group() {
return Some(group.title());
}
match self {
Self::Compare => Some(RUN),
_ => None,
}
}
pub const fn group(self) -> Option<Group> {
match self {
Self::Overview | Self::Ledger | Self::Compare => None,
Self::RothConversions | Self::SsaBenefits | Self::MonteCarlo | Self::Historical => {
Some(Group::Tools)
}
Self::Accounts
| Self::Income
| Self::Expenses
| Self::Cliffs
| Self::Transfers
| Self::Conversions
| Self::Contributions
| Self::Events
| Self::People
| Self::Residency
| Self::Household
| Self::Settings
| Self::Market => Some(Group::Plan),
}
}
pub const fn is_domain(self) -> bool {
matches!(self.group(), Some(Group::Plan))
}
pub const fn index(self) -> usize {
self as usize
}
pub const fn tab(self) -> usize {
if let Some(group) = self.group() {
return group.tab();
}
let mut tab = 0;
let mut at = 0;
while at < self.index() {
if Self::ALL[at].group().is_none() {
tab += 1;
}
at += 1;
}
tab
}
const fn own(tab: usize) -> Option<Self> {
let mut seen = 0;
let mut at = 0;
while at < Self::ALL.len() {
let page = Self::ALL[at];
if page.group().is_none() {
if seen == tab {
return Some(page);
}
seen += 1;
}
at += 1;
}
None
}
}
pub const RUN: &str = "Run";
const OWN_TABS: usize = count_own_tabs();
pub const TAB_COUNT: usize = OWN_TABS + Group::ALL.len();
const fn count_own_tabs() -> usize {
let mut count = 0;
let mut at = 0;
while at < Page::ALL.len() {
if Page::ALL[at].group().is_none() {
count += 1;
}
at += 1;
}
count
}
#[must_use]
pub fn entering(tab: usize, last: LastShown) -> Page {
Page::own(tab).unwrap_or_else(|| last.of(Group::at(tab)))
}
#[must_use]
pub fn neighbor_tab(tab: usize, step: isize) -> usize {
let count = TAB_COUNT as isize;
(tab as isize + step).rem_euclid(count) as usize
}
#[must_use]
pub fn tab_digit(tab: usize) -> char {
char::from_digit(tab as u32 + 1, 10).expect("a tab per digit")
}
pub const fn tab_title(tab: usize) -> &'static str {
match Page::own(tab) {
Some(page) => page.title(),
None => Group::at(tab).title(),
}
}
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug)]
pub struct ActivePage(pub Page);
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug)]
pub struct LastShown([Page; Group::ALL.len()]);
impl LastShown {
#[must_use]
pub fn of(self, group: Group) -> Page {
self.0[group as usize]
}
}
impl Default for LastShown {
fn default() -> Self {
Self(Group::ALL.map(Group::first))
}
}
impl Default for ActivePage {
fn default() -> Self {
Self(Page::Overview)
}
}
fn remember_the_page(active: Res<ActivePage>, mut last: ResMut<LastShown>) {
if let Some(group) = active.0.group()
&& active.is_changed()
{
last.0[group as usize] = active.0;
}
}
#[cfg(test)]
mod tests;