pub mod bars;
pub mod book;
pub mod chart;
pub mod footprint;
pub mod profile;
pub mod tape;
pub mod watchlist;
use serde::{Deserialize, Serialize};
use crate::config::PanelSpec;
use crate::source::{SourceId, Symbol};
use crate::state::AppState;
use crate::view::PanelView;
pub use bars::BarsPanel;
pub use book::BookPanel;
pub use chart::ChartPanel;
pub use footprint::FootprintPanel;
pub use profile::ProfilePanel;
pub use tape::TapePanel;
pub use watchlist::WatchlistPanel;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PanelKind {
Chart,
Book,
Tape,
Watchlist,
Footprint,
Profile,
Bars,
}
pub const PANEL_KINDS: [(&str, PanelKind); 7] = [
("Chart", PanelKind::Chart),
("Book", PanelKind::Book),
("Tape", PanelKind::Tape),
("Watchlist", PanelKind::Watchlist),
("Footprint", PanelKind::Footprint),
("Profile", PanelKind::Profile),
("Bars", PanelKind::Bars),
];
impl std::str::FromStr for PanelKind {
type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> {
PANEL_KINDS
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(text))
.map(|(_, kind)| *kind)
.ok_or_else(|| {
let names: Vec<&str> = PANEL_KINDS.iter().map(|(name, _)| *name).collect();
format!("unknown panel {text:?} ({})", names.join(" | "))
})
}
}
pub trait Panel {
fn kind(&self) -> PanelKind;
fn view(&self, state: &AppState, focus: (SourceId, &Symbol)) -> PanelView;
}
#[must_use]
pub fn build_panel(spec: &PanelSpec) -> Box<dyn Panel> {
match spec.kind {
PanelKind::Chart => Box::new(ChartPanel {
points: spec.depth_or(CHART_POINTS),
}),
PanelKind::Book => Box::new(BookPanel {
depth: spec.depth_or(DEFAULT_DEPTH),
}),
PanelKind::Tape => Box::new(TapePanel {
rows: spec.depth_or(TAPE_ROWS),
}),
PanelKind::Watchlist => Box::new(WatchlistPanel),
PanelKind::Footprint => Box::new(FootprintPanel {
depth: spec.depth_or(DEFAULT_DEPTH),
}),
PanelKind::Profile => Box::new(ProfilePanel),
PanelKind::Bars => Box::new(BarsPanel {
bars: spec.depth_or(DEFAULT_DEPTH),
}),
}
}
pub(crate) const DEFAULT_DEPTH: usize = 12;
pub(crate) const TAPE_ROWS: usize = DEFAULT_DEPTH * 2;
pub(crate) const CHART_POINTS: usize = 120;
#[cfg(test)]
mod tests {
use super::*;
use crate::config::RectSpec;
const EVERY_KIND: [PanelKind; 7] = [
PanelKind::Chart,
PanelKind::Book,
PanelKind::Tape,
PanelKind::Watchlist,
PanelKind::Footprint,
PanelKind::Profile,
PanelKind::Bars,
];
#[test]
fn a_panel_carries_the_depth_its_spec_asks_for() {
let mut spec = PanelSpec::new(PanelKind::Book, RectSpec::new(0, 0, 100, 100));
assert_eq!(spec.depth_or(12), 12, "no depth means the default");
spec.depth = Some(40);
assert_eq!(spec.depth_or(12), 40);
}
#[test]
fn a_depth_of_zero_is_refused_rather_than_honoured() {
let mut spec = PanelSpec::new(PanelKind::Tape, RectSpec::new(0, 0, 100, 100));
spec.depth = Some(0);
assert_eq!(spec.depth_or(24), 1);
}
#[test]
fn a_depth_beyond_the_ceiling_is_clamped() {
let mut spec = PanelSpec::new(PanelKind::Tape, RectSpec::new(0, 0, 100, 100));
spec.depth = Some(1_000_000);
assert_eq!(spec.depth_or(24), crate::config::MAX_PANEL_DEPTH);
}
#[test]
fn build_panel_matches_the_spec_kind() {
for kind in EVERY_KIND {
let spec = PanelSpec {
kind,
rect: RectSpec::new(0, 0, 100, 100),
depth: None,
};
assert_eq!(build_panel(&spec).kind(), kind);
}
}
}