use dioxus::prelude::*;
use crate::{
cn,
uikit::{
BookSide, OPEN_ORDERS_EMPTY, ORDER_BOOK, ORDER_BOOK_DEPTH, ORDER_BOOK_HEAD, ORDER_BOOK_PRICE, ORDER_BOOK_ROW, ORDER_BOOK_SPREAD, ORDER_FORM, ORDER_FORM_LABEL, ORDER_FORM_ROW,
ORDER_FORM_SUBMIT_BASE, ORDER_FORM_VALUE, OrderSide, TERMINAL_CHART, TERMINAL_PANE, TERMINAL_PANE_BODY, TERMINAL_PANE_HEADER, TERMINAL_ROOT, TERMINAL_TICKER, TERMINAL_TICKER_LABEL,
TERMINAL_TICKER_STAT, TERMINAL_TICKER_VALUE, TRADES_TAPE_ROW, TerminalArea, book_depth_class,
},
};
#[component]
pub fn Terminal(#[props(default)] class: String, children: Element) -> Element {
let cls = cn!(TERMINAL_ROOT, class);
rsx! {
div { class: cls, "data-slot": "terminal", {children} }
}
}
#[component]
pub fn TerminalPane(area: TerminalArea, #[props(default)] class: String, children: Element) -> Element {
let cls = cn!(TERMINAL_PANE, area.as_class(), class);
rsx! {
section { class: cls, "data-slot": "terminal-pane", "data-area": area.as_ref(), {children} }
}
}
#[component]
pub fn TerminalPaneHeader(#[props(default)] class: String, children: Element) -> Element {
let cls = cn!(TERMINAL_PANE_HEADER, class);
rsx! {
header { class: cls, "data-slot": "terminal-pane-header", {children} }
}
}
#[component]
pub fn TerminalPaneBody(#[props(default)] class: String, children: Element) -> Element {
let cls = cn!(TERMINAL_PANE_BODY, class);
rsx! {
div { class: cls, "data-slot": "terminal-pane-body", {children} }
}
}
#[component]
pub fn TerminalChart(#[props(default)] id: Option<String>, #[props(default)] class: String, children: Element) -> Element {
let cls = cn!(TERMINAL_CHART, class);
rsx! {
div { id, class: cls, "data-slot": "terminal-chart", {children} }
}
}
#[component]
pub fn TerminalTicker(#[props(default)] class: String, children: Element) -> Element {
let cls = cn!(TERMINAL_TICKER, TerminalArea::Ticker.as_class(), class);
rsx! {
div { class: cls, "data-slot": "terminal-ticker", "data-area": TerminalArea::Ticker.as_ref(), {children} }
}
}
#[component]
pub fn TickerStat(label: String, value: String, #[props(default)] class: String) -> Element {
let value_cls = cn!(TERMINAL_TICKER_VALUE, class);
rsx! {
div { class: TERMINAL_TICKER_STAT, "data-slot": "ticker-stat",
span { class: TERMINAL_TICKER_LABEL, {label} }
span { class: value_cls, {value} }
}
}
}
#[component]
pub fn OrderBook(#[props(default)] class: String, children: Element) -> Element {
let cls = cn!(ORDER_BOOK, class);
rsx! {
div { class: cls, "data-slot": "order-book", {children} }
}
}
#[component]
pub fn OrderBookHead(price: String, size: String, total: String, #[props(default)] class: String) -> Element {
let cls = cn!(ORDER_BOOK_HEAD, class);
rsx! {
div { class: cls, "data-slot": "order-book-head",
span { {price} }
span { {size} }
span { {total} }
}
}
}
#[component]
pub fn OrderBookRow(
side: BookSide,
price: String,
size: String,
total: String,
#[props(default)] depth: f64,
#[props(default)] class: String,
onclick: Option<EventHandler<MouseEvent>>,
) -> Element {
let cls = cn!(ORDER_BOOK_ROW, class);
let price_cls = cn!(ORDER_BOOK_PRICE, side.as_class());
let depth_cls = cn!(ORDER_BOOK_DEPTH, book_depth_class(side));
let width = format!("width: {}%", depth_percent(depth));
rsx! {
div {
class: cls,
"data-slot": "order-book-row",
"data-side": side.as_ref(),
onclick: move |e| { if let Some(h) = onclick { h.call(e); } },
span { class: depth_cls, style: width, "aria-hidden": "true" }
span { class: price_cls, {price} }
span { {size} }
span { {total} }
}
}
}
#[component]
pub fn OrderBookSpread(#[props(default)] class: String, children: Element) -> Element {
let cls = cn!(ORDER_BOOK_SPREAD, class);
rsx! {
div { class: cls, "data-slot": "order-book-spread", {children} }
}
}
#[component]
pub fn TradesTapeRow(side: BookSide, price: String, size: String, time: String, #[props(default)] class: String) -> Element {
let cls = cn!(TRADES_TAPE_ROW, class);
let price_cls = cn!(ORDER_BOOK_PRICE, side.as_class());
rsx! {
div { class: cls, "data-slot": "trades-tape-row", "data-side": side.as_ref(),
span { class: price_cls, {price} }
span { {size} }
span { {time} }
}
}
}
#[component]
pub fn OrderForm(#[props(default)] class: String, onsubmit: Option<EventHandler<FormEvent>>, children: Element) -> Element {
let cls = cn!(ORDER_FORM, class);
rsx! {
form {
class: cls,
"data-slot": "order-form",
onsubmit: move |e| {
e.prevent_default();
if let Some(h) = onsubmit {
h.call(e);
}
},
{children}
}
}
}
#[component]
pub fn OrderFormRow(label: String, value: String, #[props(default)] class: String) -> Element {
let cls = cn!(ORDER_FORM_ROW, class);
rsx! {
div { class: cls, "data-slot": "order-form-row",
span { class: ORDER_FORM_LABEL, {label} }
span { class: ORDER_FORM_VALUE, {value} }
}
}
}
#[component]
pub fn OrderFormSubmit(
#[props(default)] side: OrderSide,
#[props(default)] disabled: bool,
#[props(default)] class: String,
onclick: Option<EventHandler<MouseEvent>>,
children: Element,
) -> Element {
let cls = cn!(ORDER_FORM_SUBMIT_BASE, side.as_class(), class);
rsx! {
button {
class: cls,
"data-slot": "order-form-submit",
"data-side": side.as_ref(),
r#type: "submit",
disabled,
onclick: move |e| { if let Some(h) = onclick { h.call(e); } },
{children}
}
}
}
#[component]
pub fn OpenOrdersEmpty(#[props(default)] class: String, children: Element) -> Element {
let cls = cn!(OPEN_ORDERS_EMPTY, class);
rsx! {
div { class: cls, "data-slot": "open-orders-empty", {children} }
}
}
fn depth_percent(depth: f64) -> u8 {
if depth.is_nan() {
return 0;
}
(depth.clamp(0.0, 1.0) * 100.0).round() as u8
}
#[cfg(test)]
mod tests {
use super::*;
use crate::uikit::test_util::render;
#[test]
fn pane_is_placed_by_its_area() {
fn app() -> Element {
rsx! {
Terminal {
TerminalPane { area: TerminalArea::Form, "f" }
}
}
}
let html = render(app);
assert!(html.contains("data-slot=\"terminal\""), "{html}");
assert!(html.contains("lg:col-start-3"), "{html}");
assert!(html.contains("data-area=\"form\""), "{html}");
}
#[test]
fn ticker_takes_the_full_width_slot() {
fn app() -> Element {
rsx! {
TerminalTicker {
TickerStat { label: "Last", value: "1.024", class: "text-positive" }
}
}
}
let html = render(app);
assert!(html.contains("lg:col-span-3"), "{html}");
assert!(html.contains("Last"), "{html}");
assert!(html.contains("text-positive"), "the figure takes the caller's valence: {html}");
assert!(html.contains("tabular-nums"), "{html}");
}
#[test]
fn book_row_colours_the_price_and_sizes_the_depth_bar() {
fn app() -> Element {
rsx! {
OrderBookRow { side: BookSide::Ask, price: "1.030", size: "120", total: "360", depth: 0.42 }
}
}
let html = render(app);
assert!(html.contains("text-left text-accent-error"), "the price keeps its alignment beside the side colour: {html}");
assert!(html.contains("bg-accent-error/15"), "{html}");
assert!(html.contains("width: 42%"), "{html}");
assert!(html.contains("data-side=\"ask\""), "{html}");
}
#[test]
fn depth_is_clamped_to_a_percentage() {
assert_eq!(depth_percent(-0.5), 0);
assert_eq!(depth_percent(0.0), 0);
assert_eq!(depth_percent(0.5), 50);
assert_eq!(depth_percent(1.7), 100);
assert_eq!(depth_percent(f64::NAN), 0);
}
#[test]
fn tape_row_colours_the_bid_side_positive() {
fn app() -> Element {
rsx! {
TradesTapeRow { side: BookSide::Bid, price: "1.020", size: "5", time: "12:00:01" }
}
}
let html = render(app);
assert!(html.contains("text-positive"), "{html}");
assert!(html.contains("12:00:01"), "{html}");
}
#[test]
fn submit_fills_by_side() {
fn buy() -> Element {
rsx! {
OrderFormSubmit { "Buy" }
}
}
let html = render(buy);
assert!(html.contains("bg-positive"), "{html}");
assert!(html.contains("text-on-positive"), "{html}");
assert!(html.contains("type=\"submit\""), "{html}");
fn sell() -> Element {
rsx! {
OrderFormSubmit { side: OrderSide::Sell, disabled: true, "Sell" }
}
}
let html = render(sell);
assert!(html.contains("bg-accent-error"), "{html}");
assert!(html.contains("disabled"), "{html}");
}
#[test]
fn form_row_and_empty_state_render() {
fn app() -> Element {
rsx! {
OrderForm {
OrderFormRow { label: "Fee", value: "0.10" }
}
OpenOrdersEmpty { "No open orders" }
}
}
let html = render(app);
assert!(html.contains("data-slot=\"order-form\""), "{html}");
assert!(html.contains("Fee"), "{html}");
assert!(html.contains("No open orders"), "{html}");
assert!(html.contains("text-ink-soft"), "{html}");
}
}