egui_charts/ui_kit/trading_labels/
mod.rs1use crate::localization as l10n;
8use egui::{Color32, Response, RichText, Ui};
9
10use crate::scales::{CurrencyFormatter, PriceFormatter};
11use crate::theme::ThemeExt;
12
13fn side_color(ui: &Ui, gain: bool) -> Color32 {
15 let colors = ui.active_theme().ui;
16 if gain { colors.success } else { colors.error }
17}
18
19fn signed(value: f64, magnitude: String) -> (f64, String) {
23 if !magnitude.bytes().any(|b| matches!(b, b'1'..=b'9')) {
24 return (0.0, magnitude);
25 }
26 let sign = if value < 0.0 { "-" } else { "+" };
27 (value, format!("{sign}{magnitude}"))
28}
29
30pub struct PnlLabel {
49 value: f64,
50 text: String,
51 strong: bool,
52 size: Option<f32>,
53}
54
55impl PnlLabel {
56 pub fn currency(value: f64, formatter: &CurrencyFormatter) -> Self {
58 let (value, text) = signed(value, formatter.format(value.abs()));
59 Self {
60 value,
61 text,
62 strong: false,
63 size: None,
64 }
65 }
66
67 pub fn percent(value: f64) -> Self {
69 let (value, text) = signed(value, format!("{:.2}%", value.abs()));
70 Self {
71 value,
72 text,
73 strong: false,
74 size: None,
75 }
76 }
77
78 pub fn value(value: f64, text: impl Into<String>) -> Self {
80 Self {
81 value,
82 text: text.into(),
83 strong: false,
84 size: None,
85 }
86 }
87
88 #[must_use]
90 pub fn strong(mut self) -> Self {
91 self.strong = true;
92 self
93 }
94
95 #[must_use]
97 pub fn size(mut self, size: f32) -> Self {
98 self.size = Some(size);
99 self
100 }
101
102 pub fn show(self, ui: &mut Ui) -> Response {
104 let color = if self.value > 0.0 {
105 side_color(ui, true)
106 } else if self.value < 0.0 {
107 side_color(ui, false)
108 } else {
109 ui.visuals().text_color()
110 };
111 let mut rt = RichText::new(l10n::text(ui.ctx(), &self.text)).color(color);
112 if self.strong {
113 rt = rt.strong();
114 }
115 if let Some(s) = self.size {
116 rt = rt.size(s);
117 }
118 ui.label(rt)
119 }
120}
121
122pub struct SideLabel {
140 is_buy: bool,
141 text: String,
142 size: Option<f32>,
143}
144
145impl SideLabel {
146 pub fn buy() -> Self {
148 Self {
149 is_buy: true,
150 text: "BUY".to_string(),
151 size: None,
152 }
153 }
154
155 pub fn sell() -> Self {
157 Self {
158 is_buy: false,
159 text: "SELL".to_string(),
160 size: None,
161 }
162 }
163
164 pub fn long() -> Self {
166 Self {
167 is_buy: true,
168 text: "LONG".to_string(),
169 size: None,
170 }
171 }
172
173 pub fn short() -> Self {
175 Self {
176 is_buy: false,
177 text: "SHORT".to_string(),
178 size: None,
179 }
180 }
181
182 pub fn new(is_buy: bool, text: impl Into<String>) -> Self {
184 Self {
185 is_buy,
186 text: text.into(),
187 size: None,
188 }
189 }
190
191 #[must_use]
193 pub fn size(mut self, size: f32) -> Self {
194 self.size = Some(size);
195 self
196 }
197
198 pub fn show(self, ui: &mut Ui) -> Response {
200 let color = side_color(ui, self.is_buy);
201 let mut rt = RichText::new(l10n::text(ui.ctx(), &self.text)).color(color);
202 if let Some(s) = self.size {
203 rt = rt.size(s);
204 }
205 ui.label(rt)
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::{CurrencyFormatter, PnlLabel};
212
213 #[test]
214 fn pnl_text_signs_before_the_symbol_and_groups_thousands() {
215 let wrong: Vec<_> = [
216 (
217 PnlLabel::currency(12.5, &CurrencyFormatter::eur()),
218 "+12,50 €",
219 ),
220 (
221 PnlLabel::currency(-12.0, &CurrencyFormatter::new("JPY", 0)),
222 "-JPY12",
223 ),
224 (
225 PnlLabel::currency(1234.56, &CurrencyFormatter::usd()),
226 "+$1,234.56",
227 ),
228 (
229 PnlLabel::currency(-98.10, &CurrencyFormatter::usd()),
230 "-$98.10",
231 ),
232 (
233 PnlLabel::currency(1_234_567.891, &CurrencyFormatter::usd()),
234 "+$1,234,567.89",
235 ),
236 (PnlLabel::currency(0.0, &CurrencyFormatter::usd()), "$0.00"),
237 (PnlLabel::currency(-0.0, &CurrencyFormatter::usd()), "$0.00"),
238 (
239 PnlLabel::currency(-0.001, &CurrencyFormatter::usd()),
240 "$0.00",
241 ),
242 (PnlLabel::percent(5.34), "+5.34%"),
243 (PnlLabel::percent(-1.28), "-1.28%"),
244 (PnlLabel::percent(0.0), "0.00%"),
245 (PnlLabel::percent(-0.0), "0.00%"),
246 ]
247 .into_iter()
248 .filter(|(label, expected)| label.text != *expected)
249 .map(|(label, expected)| (label.text, expected))
250 .collect();
251 assert!(wrong.is_empty(), "(got, expected): {wrong:?}");
252 assert_eq!(
254 PnlLabel::currency(-0.001, &CurrencyFormatter::usd()).value,
255 0.0
256 );
257 assert_eq!(PnlLabel::percent(0.004).value, 0.0);
258 }
259}