Skip to main content

egui_charts/ui_kit/trading_labels/
mod.rs

1//! Trading label widgets: `PnlLabel` and `SideLabel`.
2//!
3//! Pre-styled labels for common trading UI patterns:
4//! - `PnlLabel`: a profit or loss coloured by its sign (green, red or neutral)
5//! - `SideLabel`: coloured buy and sell or long and short badges
6
7use crate::localization as l10n;
8use egui::{Color32, Response, RichText, Ui};
9
10use crate::scales::{CurrencyFormatter, PriceFormatter};
11use crate::theme::ThemeExt;
12
13/// The theme's success colour for a gain, its error colour for a loss.
14fn side_color(ui: &Ui, gain: bool) -> Color32 {
15    let colors = ui.active_theme().ui;
16    if gain { colors.success } else { colors.error }
17}
18
19/// `magnitude` (a formatted absolute value) with `+` for a gain or `-` for a loss, and no sign
20/// when it shows zero. The value is also the one the label is coloured by: zero when the text
21/// shows zero.
22fn 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
30// =============================================================================
31// PnlLabel
32// =============================================================================
33
34/// Auto-colored profit/loss label.
35///
36/// Colors text with the theme's success or error colour by the value's sign, and zero in the
37/// normal text colour.
38///
39/// ```
40/// # use egui_charts::{ui_kit::trading_labels::PnlLabel, scales::CurrencyFormatter};
41/// # egui::__run_test_ui(|ui| {
42/// PnlLabel::currency(1234.5, &CurrencyFormatter::usd()).show(ui);       // "+$1,234.50" in green
43/// PnlLabel::currency(-50.0, &CurrencyFormatter::usd()).show(ui);        // "-$50.00" in red
44/// PnlLabel::percent(5.34).show(ui);          // "+5.34%" in green
45/// PnlLabel::value(-10.0, "custom").show(ui); // "custom" in red
46/// # });
47/// ```
48pub struct PnlLabel {
49    value: f64,
50    text: String,
51    strong: bool,
52    size: Option<f32>,
53}
54
55impl PnlLabel {
56    /// Display a P&L value using the instrument currency and precision supplied by the host.
57    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    /// Display a percentage P&L value given in percent units (`5.34` shows "+5.34%")
68    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    /// Display custom text colored by the given value's sign
79    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    /// Make the label bold
89    #[must_use]
90    pub fn strong(mut self) -> Self {
91        self.strong = true;
92        self
93    }
94
95    /// Set the font size
96    #[must_use]
97    pub fn size(mut self, size: f32) -> Self {
98        self.size = Some(size);
99        self
100    }
101
102    /// Show the label
103    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
122// =============================================================================
123// SideLabel
124// =============================================================================
125
126/// Colored buy/sell or long/short badge.
127///
128/// Uses the theme's success colour for buy/long and its error colour for sell/short.
129///
130/// ```
131/// # use egui_charts::ui_kit::trading_labels::SideLabel;
132/// # egui::__run_test_ui(|ui| {
133/// SideLabel::buy().show(ui);   // "BUY" in green
134/// SideLabel::sell().show(ui);  // "SELL" in red
135/// SideLabel::long().show(ui);  // "LONG" in green
136/// SideLabel::short().show(ui); // "SHORT" in red
137/// # });
138/// ```
139pub struct SideLabel {
140    is_buy: bool,
141    text: String,
142    size: Option<f32>,
143}
144
145impl SideLabel {
146    /// "BUY" label in bullish color
147    pub fn buy() -> Self {
148        Self {
149            is_buy: true,
150            text: "BUY".to_string(),
151            size: None,
152        }
153    }
154
155    /// "SELL" label in bearish color
156    pub fn sell() -> Self {
157        Self {
158            is_buy: false,
159            text: "SELL".to_string(),
160            size: None,
161        }
162    }
163
164    /// "LONG" label in bullish color
165    pub fn long() -> Self {
166        Self {
167            is_buy: true,
168            text: "LONG".to_string(),
169            size: None,
170        }
171    }
172
173    /// "SHORT" label in bearish color
174    pub fn short() -> Self {
175        Self {
176            is_buy: false,
177            text: "SHORT".to_string(),
178            size: None,
179        }
180    }
181
182    /// Custom side label with the given text
183    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    /// Set the font size
192    #[must_use]
193    pub fn size(mut self, size: f32) -> Self {
194        self.size = Some(size);
195        self
196    }
197
198    /// Show the label
199    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        // Text that shows zero takes the zero colour, whatever the value was.
253        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}