use egui::{
Align2, Color32, CornerRadius, FontId, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, Vec2,
};
use crate::ext::HasDesignTokens;
use crate::styles::typography;
use crate::tokens::DESIGN_TOKENS;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OrderId(pub String);
impl OrderId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
}
impl std::fmt::Display for OrderId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderSide {
Buy,
Sell,
}
impl OrderSide {
pub fn label(&self) -> &'static str {
match self {
OrderSide::Buy => "BUY",
OrderSide::Sell => "SELL",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum OrderLineType {
Entry,
StopLoss,
TakeProfit { partial_pct: Option<f64> },
PendingLimit,
PendingStop,
}
impl OrderLineType {
pub fn short_label(&self) -> &'static str {
match self {
OrderLineType::Entry => "ENT",
OrderLineType::StopLoss => "SL",
OrderLineType::TakeProfit { .. } => "TP",
OrderLineType::PendingLimit => "LMT",
OrderLineType::PendingStop => "STP",
}
}
}
#[derive(Debug, Clone)]
pub struct OrderLine {
pub order_id: OrderId,
pub price: f64,
pub line_type: OrderLineType,
pub label: String,
pub side: OrderSide,
pub quantity: f64,
pub is_active: bool,
pub is_selected: bool,
}
impl OrderLine {
pub fn entry(
order_id: impl Into<String>,
price: f64,
side: OrderSide,
quantity: f64,
label: impl Into<String>,
) -> Self {
Self {
order_id: OrderId::new(order_id),
price,
line_type: OrderLineType::Entry,
label: label.into(),
side,
quantity,
is_active: true,
is_selected: false,
}
}
pub fn stop_loss(
order_id: impl Into<String>,
price: f64,
side: OrderSide,
quantity: f64,
label: impl Into<String>,
) -> Self {
Self {
order_id: OrderId::new(order_id),
price,
line_type: OrderLineType::StopLoss,
label: label.into(),
side,
quantity,
is_active: true,
is_selected: false,
}
}
pub fn take_profit(
order_id: impl Into<String>,
price: f64,
side: OrderSide,
quantity: f64,
label: impl Into<String>,
partial_pct: Option<f64>,
) -> Self {
Self {
order_id: OrderId::new(order_id),
price,
line_type: OrderLineType::TakeProfit { partial_pct },
label: label.into(),
side,
quantity,
is_active: true,
is_selected: false,
}
}
pub fn pending_limit(
order_id: impl Into<String>,
price: f64,
side: OrderSide,
quantity: f64,
label: impl Into<String>,
) -> Self {
Self {
order_id: OrderId::new(order_id),
price,
line_type: OrderLineType::PendingLimit,
label: label.into(),
side,
quantity,
is_active: true,
is_selected: false,
}
}
pub fn with_selected(mut self, selected: bool) -> Self {
self.is_selected = selected;
self
}
pub fn with_active(mut self, active: bool) -> Self {
self.is_active = active;
self
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum OrderLineAction {
#[default]
None,
Selected(OrderId),
CancelRequested(OrderId),
DragStarted(OrderId),
Dragging(OrderId, f64),
DragCompleted(OrderId, f64),
}
#[derive(Debug, Clone, Default)]
pub struct OrderLineOverlay {
pub lines: Vec<OrderLine>,
}
impl OrderLineOverlay {
pub fn new() -> Self {
Self { lines: Vec::new() }
}
pub fn add_line(&mut self, line: OrderLine) {
self.lines.push(line);
}
pub fn remove_line(&mut self, order_id: &OrderId) {
self.lines.retain(|l| &l.order_id != order_id);
}
pub fn clear(&mut self) {
self.lines.clear();
}
pub fn get_line(&self, order_id: &OrderId) -> Option<&OrderLine> {
self.lines.iter().find(|l| &l.order_id == order_id)
}
pub fn get_line_mut(&mut self, order_id: &OrderId) -> Option<&mut OrderLine> {
self.lines.iter_mut().find(|l| &l.order_id == order_id)
}
pub fn render(
&self,
ui: &mut Ui,
chart_rect: Rect,
price_to_y: impl Fn(f64) -> f32,
) -> OrderLineAction {
let mut action = OrderLineAction::None;
for line in &self.lines {
let y = price_to_y(line.price);
if y < chart_rect.top() - 20.0 || y > chart_rect.bottom() + 20.0 {
continue;
}
let line_action = render_order_line(ui, chart_rect, y, line);
if !matches!(line_action, OrderLineAction::None) {
action = line_action;
}
}
action
}
}
fn render_order_line(ui: &mut Ui, chart_rect: Rect, y: f32, line: &OrderLine) -> OrderLineAction {
let mut action = OrderLineAction::None;
let (line_color, label_bg, label_text_color) = get_order_colors(ui, line);
let bearish = ui.bearish_color();
let line_width = if line.is_selected {
DESIGN_TOKENS.spacing.xs
} else {
DESIGN_TOKENS.spacing.hairline
};
let label_width = DESIGN_TOKENS.sizing.charts_ext.order_line_label_width;
let label_height = DESIGN_TOKENS.sizing.charts_ext.order_line_label_height;
let label_rect = Rect::from_min_size(
Pos2::new(
chart_rect.right() - label_width - DESIGN_TOKENS.spacing.lg,
y - label_height / 2.0,
),
Vec2::new(label_width, label_height),
);
let cancel_size = DESIGN_TOKENS.sizing.icon_sm;
let cancel_rect = Rect::from_min_size(
Pos2::new(
label_rect.right() + DESIGN_TOKENS.spacing.xs,
y - cancel_size / 2.0,
),
Vec2::splat(cancel_size),
);
let type_rect = Rect::from_min_size(
Pos2::new(
chart_rect.left() + DESIGN_TOKENS.spacing.sm,
y - DESIGN_TOKENS.spacing.lg,
),
Vec2::new(DESIGN_TOKENS.sizing.button_md, DESIGN_TOKENS.sizing.icon_sm),
);
let label_response = ui.allocate_rect(label_rect, Sense::click());
if label_response.clicked() {
action = OrderLineAction::Selected(line.order_id.clone());
}
let cancel_response = if line.is_active {
let resp = ui.allocate_rect(cancel_rect, Sense::click());
if resp.clicked() {
action = OrderLineAction::CancelRequested(line.order_id.clone());
}
Some(resp)
} else {
None
};
let painter = ui.painter();
let dash_length = 6.0;
let gap_length = 4.0;
let mut x = chart_rect.left();
while x < chart_rect.right() - 100.0 {
let segment_end = (x + dash_length).min(chart_rect.right() - 100.0);
painter.line_segment(
[Pos2::new(x, y), Pos2::new(segment_end, y)],
Stroke::new(line_width, line_color),
);
x += dash_length + gap_length;
}
painter.rect_filled(label_rect, CornerRadius::same(3), label_bg);
if line.is_selected {
painter.rect_stroke(
label_rect,
CornerRadius::same(3),
Stroke::new(DESIGN_TOKENS.stroke.hairline, line_color),
StrokeKind::Inside,
);
}
let label_text_display = if line.label.len() > 18 {
format!("{}...", &line.label[..15])
} else {
line.label.clone()
};
painter.text(
label_rect.center(),
Align2::CENTER_CENTER,
&label_text_display,
FontId::proportional(typography::SM),
label_text_color,
);
if let Some(cancel_resp) = cancel_response {
let cancel_bg = if cancel_resp.hovered() {
bearish
} else {
Color32::from_rgba_unmultiplied(bearish.r(), bearish.g(), bearish.b(), 180)
};
painter.rect_filled(cancel_rect, CornerRadius::same(2), cancel_bg);
let x_icon_color = DESIGN_TOKENS.semantic.chart.crosshair_label_text;
let x_margin = 4.0;
painter.line_segment(
[
cancel_rect.left_top() + Vec2::splat(x_margin),
cancel_rect.right_bottom() - Vec2::splat(x_margin),
],
Stroke::new(DESIGN_TOKENS.stroke.medium, x_icon_color),
);
painter.line_segment(
[
cancel_rect.right_top() + Vec2::new(-x_margin, x_margin),
cancel_rect.left_bottom() + Vec2::new(x_margin, -x_margin),
],
Stroke::new(DESIGN_TOKENS.stroke.medium, x_icon_color),
);
}
let type_label = line.line_type.short_label();
painter.rect_filled(type_rect, CornerRadius::same(2), label_bg);
painter.text(
type_rect.center(),
Align2::CENTER_CENTER,
type_label,
FontId::proportional(typography::XS),
label_text_color,
);
action
}
fn get_order_colors(ui: &Ui, line: &OrderLine) -> (Color32, Color32, Color32) {
let bullish = ui.bullish_color();
let bearish = ui.bearish_color();
match line.line_type {
OrderLineType::Entry | OrderLineType::PendingLimit | OrderLineType::PendingStop => {
let base = match line.side {
OrderSide::Buy => bullish,
OrderSide::Sell => bearish,
};
let [r, g, b, _] = base.to_array();
(
base,
Color32::from_rgba_unmultiplied(r, g, b, 220),
DESIGN_TOKENS.semantic.chart.crosshair_label_text,
)
}
OrderLineType::StopLoss => {
let [r, g, b, _] = bearish.to_array();
(
bearish,
Color32::from_rgba_unmultiplied(r, g, b, 220),
DESIGN_TOKENS.semantic.chart.crosshair_label_text,
)
}
OrderLineType::TakeProfit { .. } => {
let [r, g, b, _] = bullish.to_array();
(
bullish,
Color32::from_rgba_unmultiplied(r, g, b, 220),
DESIGN_TOKENS.semantic.chart.crosshair_label_text,
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_order_line_creation() {
let line = OrderLine::entry("order1", 100.0, OrderSide::Buy, 1.0, "Test Order");
assert_eq!(line.order_id.0, "order1");
assert_eq!(line.price, 100.0);
assert!(line.is_active);
}
#[test]
fn test_overlay_management() {
let mut overlay = OrderLineOverlay::new();
overlay.add_line(OrderLine::entry(
"o1",
100.0,
OrderSide::Buy,
1.0,
"Order 1",
));
overlay.add_line(OrderLine::stop_loss("o2", 95.0, OrderSide::Buy, 1.0, "SL"));
assert_eq!(overlay.lines.len(), 2);
overlay.remove_line(&OrderId::new("o1"));
assert_eq!(overlay.lines.len(), 1);
overlay.clear();
assert!(overlay.lines.is_empty());
}
}