Skip to main content

egui_charts/model/
watchlist.rs

1//! Watchlists as plain data: named lists of symbols and sections with colour flags and text
2//! import and export. The panel in `ui::widget_bar` shows them and the settings store keeps them.
3
4use egui::Color32;
5use serde::{Deserialize, Serialize};
6
7/// A colour flag marking a watchlist symbol.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub enum WatchlistFlag {
10    /// Red watchlist flag.
11    Red,
12    /// Blue watchlist flag.
13    Blue,
14    /// Green watchlist flag.
15    Green,
16    /// Orange watchlist flag.
17    Orange,
18    /// Purple watchlist flag.
19    Purple,
20    /// Cyan watchlist flag.
21    Cyan,
22    /// Pink watchlist flag.
23    Pink,
24}
25
26impl WatchlistFlag {
27    /// Every flag, in menu order.
28    pub const ALL: [Self; 7] = [
29        Self::Red,
30        Self::Blue,
31        Self::Green,
32        Self::Orange,
33        Self::Purple,
34        Self::Cyan,
35        Self::Pink,
36    ];
37
38    /// The flag colour, the same in every theme.
39    pub fn color(self) -> Color32 {
40        match self {
41            Self::Red => Color32::from_rgb(229, 57, 53),
42            Self::Blue => Color32::from_rgb(30, 136, 229),
43            Self::Green => Color32::from_rgb(67, 160, 71),
44            Self::Orange => Color32::from_rgb(251, 140, 0),
45            Self::Purple => Color32::from_rgb(142, 36, 170),
46            Self::Cyan => Color32::from_rgb(0, 172, 193),
47            Self::Pink => Color32::from_rgb(216, 27, 96),
48        }
49    }
50}
51
52/// One row of a watchlist.
53#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
54pub enum WatchlistItem {
55    /// A symbol, optionally flagged.
56    Symbol {
57        /// Instrument identifier used by the host.
58        symbol: String,
59        /// Optional user-assigned color flag.
60        flag: Option<WatchlistFlag>,
61    },
62    /// A section title grouping the symbols below it, up to the next section.
63    Section(String),
64}
65
66/// A named list of symbols and sections, in the order the user arranged them.
67#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
68pub struct Watchlist {
69    /// Display name.
70    pub name: String,
71    /// Symbols and section headings in display order.
72    pub items: Vec<WatchlistItem>,
73}
74
75impl Watchlist {
76    /// An empty list.
77    pub fn new(name: impl Into<String>) -> Self {
78        Self {
79            name: name.into(),
80            items: Vec::new(),
81        }
82    }
83
84    /// A copy named after this list and `taken` names: "Name copy", then "Name copy 2" and so on.
85    /// Sections, symbol order and flags carry over.
86    pub fn duplicate<'a>(&self, taken: impl IntoIterator<Item = &'a str>) -> Self {
87        let taken: Vec<&str> = taken.into_iter().collect();
88        let base = format!("{} copy", self.name);
89        let name = std::iter::once(base.clone())
90            .chain((2..).map(|n| format!("{base} {n}")))
91            .find(|name| !taken.contains(&name.as_str()))
92            .expect("an unused name exists");
93        Self {
94            name,
95            items: self.items.clone(),
96        }
97    }
98
99    /// The symbols of the list, in list order.
100    pub fn symbols(&self) -> impl Iterator<Item = &str> {
101        self.items.iter().filter_map(|item| match item {
102            WatchlistItem::Symbol { symbol, .. } => Some(symbol.as_str()),
103            WatchlistItem::Section(_) => None,
104        })
105    }
106
107    /// Appends `symbol`, trimmed. Returns false, leaving the list unchanged, for an empty symbol,
108    /// one containing a text separator or starting with `###`, or one the list already holds.
109    pub fn add_symbol(&mut self, symbol: &str) -> bool {
110        let symbol = symbol.trim();
111        if symbol.is_empty()
112            || symbol.contains([',', '\n', '\r'])
113            || symbol.starts_with("###")
114            || self.symbols().any(|s| s == symbol)
115        {
116            return false;
117        }
118        self.items.push(WatchlistItem::Symbol {
119            symbol: symbol.to_owned(),
120            flag: None,
121        });
122        true
123    }
124
125    /// The list as comma-separated text, each section written as `###` and its title, the format
126    /// [`from_text`](Self::from_text) reads. Flags are not exported. Commas and line breaks in a
127    /// section title become spaces.
128    pub fn to_text(&self) -> String {
129        let entries: Vec<String> = self
130            .items
131            .iter()
132            .map(|item| match item {
133                WatchlistItem::Symbol { symbol, .. } => symbol.clone(),
134                WatchlistItem::Section(title) => {
135                    format!("###{}", title.replace([',', '\n', '\r'], " "))
136                }
137            })
138            .collect();
139        entries.join(",")
140    }
141
142    /// A list named `name` from text separated by commas or line breaks. An entry starting with
143    /// `###` is a section title; every other entry is a symbol. Entries are trimmed; empty
144    /// entries and symbols already listed are skipped.
145    pub fn from_text(name: impl Into<String>, text: &str) -> Self {
146        let mut list = Self::new(name);
147        for entry in text.split([',', '\n', '\r']).map(str::trim) {
148            if let Some(title) = entry.strip_prefix("###") {
149                list.items
150                    .push(WatchlistItem::Section(title.trim().to_owned()));
151            } else {
152                list.add_symbol(entry);
153            }
154        }
155        list
156    }
157}