use crate::app::{App, Mode};
use crate::ui::graph::{GraphCache, GraphProjection};
use optionstratlib::visualization::{GraphData, Series2D};
#[derive(Debug, Clone)]
pub struct ViewState {
payoff: PayoffView,
surface: SurfaceViewCache,
replay: ReplayView,
replay_payoff: ReplayPayoffView,
}
impl Default for ViewState {
fn default() -> Self {
Self::new()
}
}
impl ViewState {
#[must_use]
pub fn new() -> Self {
Self {
payoff: PayoffView::new(),
surface: SurfaceViewCache::new(),
replay: ReplayView::new(),
replay_payoff: ReplayPayoffView::new(),
}
}
pub fn sync(&mut self, app: &App) {
match &app.mode {
Mode::Live(live) => {
self.payoff.sync(live.payoff_builder.graph_revision(), || {
live.payoff_builder.active_graph_data().clone()
});
self.surface.sync(live.surface.graph_revision(), || {
live.surface.active_graph_data().clone()
});
}
Mode::Replay(replay) => match replay.loaded() {
Some(loaded) => {
self.replay
.sync(loaded.equity_revision(), || loaded.equity_graph().clone());
self.replay_payoff
.sync(loaded.payoff_revision(), || loaded.payoff_graph().clone());
}
None => {
self.replay.reset();
self.replay_payoff.reset();
}
},
}
}
#[must_use]
pub fn payoff(&self) -> &GraphProjection {
self.payoff.cache.projection()
}
#[must_use]
pub fn surface(&self) -> &GraphProjection {
self.surface.cache.projection()
}
#[must_use]
pub fn replay(&self) -> &GraphProjection {
self.replay.cache.projection()
}
#[must_use]
pub fn replay_payoff(&self) -> &GraphProjection {
self.replay_payoff.cache.projection()
}
}
#[derive(Debug, Clone)]
struct PayoffView {
cache: GraphCache,
projected_for: Option<u64>,
}
impl PayoffView {
fn new() -> Self {
Self {
cache: GraphCache::new(GraphData::Series(Series2D::default())),
projected_for: None,
}
}
fn sync(&mut self, revision: u64, source: impl FnOnce() -> GraphData) {
if self.projected_for != Some(revision) {
self.cache.update(source());
self.projected_for = Some(revision);
}
}
}
#[derive(Debug, Clone)]
struct SurfaceViewCache {
cache: GraphCache,
projected_for: Option<u64>,
}
impl SurfaceViewCache {
fn new() -> Self {
Self {
cache: GraphCache::new(GraphData::Series(Series2D::default())),
projected_for: None,
}
}
fn sync(&mut self, revision: u64, source: impl FnOnce() -> GraphData) {
if self.projected_for != Some(revision) {
self.cache.update(source());
self.projected_for = Some(revision);
}
}
}
#[derive(Debug, Clone)]
struct ReplayView {
cache: GraphCache,
projected_for: Option<u64>,
}
impl ReplayView {
fn new() -> Self {
Self {
cache: GraphCache::new(GraphData::Series(Series2D::default())),
projected_for: None,
}
}
fn sync(&mut self, revision: u64, source: impl FnOnce() -> GraphData) {
if self.projected_for != Some(revision) {
self.cache.update(source());
self.projected_for = Some(revision);
}
}
fn reset(&mut self) {
if self.projected_for.is_some() {
self.cache.update(GraphData::Series(Series2D::default()));
self.projected_for = None;
}
}
}
#[derive(Debug, Clone)]
struct ReplayPayoffView {
cache: GraphCache,
projected_for: Option<u64>,
}
impl ReplayPayoffView {
fn new() -> Self {
Self {
cache: GraphCache::new(GraphData::Series(Series2D::default())),
projected_for: None,
}
}
fn sync(&mut self, revision: u64, source: impl FnOnce() -> GraphData) {
if self.projected_for != Some(revision) {
self.cache.update(source());
self.projected_for = Some(revision);
}
}
fn reset(&mut self) {
if self.projected_for.is_some() {
self.cache.update(GraphData::Series(Series2D::default()));
self.projected_for = None;
}
}
}
#[cfg(test)]
mod tests {
use super::ViewState;
use crate::app::tests_support::{live_app_on, ready_replay_app, ready_replay_app_with_fills};
use crate::app::{LiveScreen, ScreenLoad};
use crate::event::SeekTo;
#[test]
fn test_sync_projects_replay_equity_for_a_ready_bundle() {
let (replay, _rx) = ready_replay_app_with_fills(6);
let mut view = ViewState::new();
view.sync(&replay);
assert!(
view.replay().ready().is_some(),
"a Ready bundle projects a renderable equity series",
);
let live = live_app_on(LiveScreen::Chain, ScreenLoad::Ready, false);
let mut live_view = ViewState::new();
live_view.sync(&live);
assert!(
live_view.replay().empty_reason().is_some(),
"a live app leaves the replay cache empty",
);
}
#[test]
fn test_sync_reprojects_only_when_the_equity_revision_changes() {
let (mut replay, _rx) = ready_replay_app_with_fills(6);
let mut view = ViewState::new();
view.sync(&replay);
let first = view.replay().clone();
view.sync(&replay);
assert_eq!(
&first,
view.replay(),
"an unchanged bundle does not re-project"
);
replay.on_event(crate::event::AppEvent::ReplaySeek(SeekTo::Step(2)));
view.sync(&replay);
assert_ne!(
&first,
view.replay(),
"a cursor move re-projects the equity series to the new head",
);
}
#[test]
fn test_sync_resets_replay_cache_when_the_bundle_is_not_ready() {
let (_ready, _rx) = ready_replay_app(6);
let loading =
crate::app::tests_support::replay_app_on(crate::app::ReplayScreen::Replay, false);
let mut view = ViewState::new();
view.sync(&loading);
assert!(
view.replay().empty_reason().is_some(),
"a loading bundle leaves the replay cache empty",
);
}
}