441 lines
14 KiB
Rust
441 lines
14 KiB
Rust
use std::{ops::Range, rc::Rc};
|
|
|
|
use gpui::{Hsla, IntoElement, PathBuilder, Pixels, Point, Styled, Window, canvas, fill};
|
|
|
|
use super::{
|
|
candles::Candle,
|
|
market::ChartKind,
|
|
series::Drawn,
|
|
stage::Visible,
|
|
tools::{self, Drawing},
|
|
};
|
|
use crate::{
|
|
charts::{Linear, Rect, at, finish, place, tint},
|
|
theme::Palette,
|
|
};
|
|
|
|
/// How a market chart paints: its colors and measures in pixels.
|
|
#[derive(Clone)]
|
|
pub(crate) struct Pen {
|
|
pub palette: Palette,
|
|
pub rise: Hsla,
|
|
pub fall: Hsla,
|
|
pub stroke: Pixels,
|
|
pub hairline: Pixels,
|
|
}
|
|
|
|
/// What a market chart paints, in its box's own pixels: panes with their price scales, main first; the candles in view and how they draw; volume, profile, overlays, studies, a compared symbol, the last price, alerts, and the crosshair.
|
|
pub(crate) struct Scene {
|
|
pub panes: Vec<(Rect, Linear, Vec<f64>)>,
|
|
pub visible: Visible,
|
|
pub range: Range<usize>,
|
|
pub candles: Rc<Vec<Candle>>,
|
|
pub kind: ChartKind,
|
|
pub volume: Option<f64>,
|
|
pub profile: Vec<(f64, f64, f64)>,
|
|
pub overlays: Rc<Vec<Drawn>>,
|
|
pub studies: Rc<Vec<Drawn>>,
|
|
pub compare: Option<Vec<Option<f64>>>,
|
|
pub last: Option<(f64, bool)>,
|
|
pub alerts: Vec<f64>,
|
|
pub cross: Option<(i64, Option<(usize, f32)>)>,
|
|
pub drawings: Vec<Drawing>,
|
|
pub pen: Pen,
|
|
}
|
|
|
|
impl Scene {
|
|
fn x(&self, ix: usize) -> f32 {
|
|
let main = self.panes[0].0;
|
|
self.visible.x(ix as f64, (main.x, main.w))
|
|
}
|
|
|
|
fn slot(&self) -> f32 {
|
|
self.visible.slot(self.panes[0].0.w)
|
|
}
|
|
}
|
|
|
|
/// A straight line in dashes.
|
|
fn dashed(
|
|
origin: Point<Pixels>,
|
|
(from, to): ((f32, f32), (f32, f32)),
|
|
color: Hsla,
|
|
pen: &Pen,
|
|
window: &mut Window,
|
|
) {
|
|
let dash = [pen.stroke * 2.0, pen.stroke * 2.0];
|
|
let mut path = PathBuilder::stroke(pen.hairline).dash_array(&dash);
|
|
path.move_to(at(origin, from));
|
|
path.line_to(at(origin, to));
|
|
finish(path, color, window);
|
|
}
|
|
|
|
/// A reading's line through the candles in view, broken where it has no value.
|
|
fn reading(
|
|
scene: &Scene,
|
|
values: &[Option<f64>],
|
|
scale: &Linear,
|
|
width: Pixels,
|
|
color: Hsla,
|
|
origin: Point<Pixels>,
|
|
window: &mut Window,
|
|
) {
|
|
let mut path = PathBuilder::stroke(width);
|
|
let mut open = false;
|
|
for ix in scene.range.clone() {
|
|
match values.get(ix).copied().flatten() {
|
|
Some(value) => {
|
|
let place = at(origin, (scene.x(ix), scale.at(value)));
|
|
if open {
|
|
path.line_to(place)
|
|
} else {
|
|
path.move_to(place)
|
|
}
|
|
open = true;
|
|
}
|
|
None => open = false,
|
|
}
|
|
}
|
|
finish(path, color, window);
|
|
}
|
|
|
|
/// The price's own marks: candles, bars, or a line with or without its area.
|
|
fn prices(scene: &Scene, origin: Point<Pixels>, window: &mut Window) {
|
|
let (main, scale) = (scene.panes[0].0, &scene.panes[0].1);
|
|
let (pen, slot) = (&scene.pen, scene.slot());
|
|
let body = (slot * 0.68).max(f32::from(pen.hairline));
|
|
if matches!(scene.kind, ChartKind::Line | ChartKind::Area) {
|
|
let closes: Vec<Option<f64>> = scene
|
|
.candles
|
|
.iter()
|
|
.map(|candle| Some(candle.close))
|
|
.collect();
|
|
let ink = tint(&pen.palette, 0);
|
|
if scene.kind == ChartKind::Area && scene.range.len() > 1 {
|
|
let mut area = PathBuilder::fill();
|
|
let (first, last) = (scene.range.start, scene.range.end - 1);
|
|
area.move_to(at(origin, (scene.x(first), main.y + main.h)));
|
|
scene.range.clone().for_each(|ix| {
|
|
area.line_to(at(origin, (scene.x(ix), scale.at(scene.candles[ix].close))))
|
|
});
|
|
area.line_to(at(origin, (scene.x(last), main.y + main.h)));
|
|
area.close();
|
|
finish(area, ink.opacity(0.12), window);
|
|
}
|
|
reading(scene, &closes, scale, pen.stroke, ink, origin, window);
|
|
return;
|
|
}
|
|
for ix in scene.range.clone() {
|
|
let (candle, x) = (&scene.candles[ix], scene.x(ix));
|
|
let ink = if candle.rose() { pen.rise } else { pen.fall };
|
|
let (high, low, open, close) = (
|
|
scale.at(candle.high),
|
|
scale.at(candle.low),
|
|
scale.at(candle.open),
|
|
scale.at(candle.close),
|
|
);
|
|
let thin = f32::from(pen.hairline);
|
|
window.paint_quad(fill(
|
|
place(
|
|
origin,
|
|
Rect {
|
|
x: x - thin / 2.0,
|
|
y: high,
|
|
w: thin,
|
|
h: low - high,
|
|
},
|
|
),
|
|
ink,
|
|
));
|
|
if scene.kind == ChartKind::Bars {
|
|
let tick = body / 2.0;
|
|
window.paint_quad(fill(
|
|
place(
|
|
origin,
|
|
Rect {
|
|
x: x - tick,
|
|
y: open - thin / 2.0,
|
|
w: tick,
|
|
h: thin,
|
|
},
|
|
),
|
|
ink,
|
|
));
|
|
window.paint_quad(fill(
|
|
place(
|
|
origin,
|
|
Rect {
|
|
x,
|
|
y: close - thin / 2.0,
|
|
w: tick,
|
|
h: thin,
|
|
},
|
|
),
|
|
ink,
|
|
));
|
|
continue;
|
|
}
|
|
let (top, bottom) = (open.min(close), open.max(close));
|
|
let rect = Rect {
|
|
x: x - body / 2.0,
|
|
y: top,
|
|
w: body,
|
|
h: (bottom - top).max(thin),
|
|
};
|
|
window.paint_quad(fill(place(origin, rect), ink));
|
|
}
|
|
}
|
|
|
|
/// Volume as faint bars along the bottom of the price pane, and the volume at each price as bars from its right edge.
|
|
fn volume(scene: &Scene, origin: Point<Pixels>, window: &mut Window) {
|
|
let (main, pen) = (scene.panes[0].0, &scene.pen);
|
|
if let Some(most) = scene.volume.filter(|most| *most > 0.0) {
|
|
let (body, room) = (
|
|
(scene.slot() * 0.68).max(f32::from(pen.hairline)),
|
|
main.h * 0.2,
|
|
);
|
|
for ix in scene.range.clone() {
|
|
let candle = &scene.candles[ix];
|
|
let tall = room * (candle.volume / most) as f32;
|
|
let ink = if candle.rose() { pen.rise } else { pen.fall };
|
|
let rect = Rect {
|
|
x: scene.x(ix) - body / 2.0,
|
|
y: main.y + main.h - tall,
|
|
w: body,
|
|
h: tall,
|
|
};
|
|
window.paint_quad(fill(place(origin, rect), ink.opacity(0.22)));
|
|
}
|
|
}
|
|
let most = scene
|
|
.profile
|
|
.iter()
|
|
.map(|(_, _, volume)| *volume)
|
|
.fold(0.0, f64::max);
|
|
for (low, high, traded) in scene.profile.iter().filter(|_| most > 0.0) {
|
|
let scale = &scene.panes[0].1;
|
|
let (top, bottom) = (scale.at(*high), scale.at(*low));
|
|
let wide = main.w * 0.24 * (*traded / most) as f32;
|
|
let strength = if *traded == most { 0.16 } else { 0.08 };
|
|
let seam = f32::from(pen.hairline);
|
|
let rect = Rect {
|
|
x: main.x + main.w - wide,
|
|
y: top + seam,
|
|
w: wide,
|
|
h: (bottom - top - seam).max(0.0),
|
|
};
|
|
window.paint_quad(fill(place(origin, rect), pen.palette.fg.opacity(strength)));
|
|
}
|
|
}
|
|
|
|
/// Overlays over the prices: a shaded band, then their lines.
|
|
fn overlays(scene: &Scene, origin: Point<Pixels>, window: &mut Window) {
|
|
let (scale, pen) = (&scene.panes[0].1, &scene.pen);
|
|
for drawn in scene.overlays.iter() {
|
|
let color = tint(
|
|
&pen.palette,
|
|
drawn.lines.first().map_or(0, |(color, _)| *color),
|
|
);
|
|
if let Some((low, high)) = &drawn.band {
|
|
let shown: Vec<usize> = scene
|
|
.range
|
|
.clone()
|
|
.filter(|ix| low[*ix].is_some() && high[*ix].is_some())
|
|
.collect();
|
|
if shown.len() > 1 {
|
|
let mut band = PathBuilder::fill();
|
|
band.move_to(at(
|
|
origin,
|
|
(scene.x(shown[0]), scale.at(high[shown[0]].expect("shown"))),
|
|
));
|
|
shown.iter().for_each(|ix| {
|
|
band.line_to(at(
|
|
origin,
|
|
(scene.x(*ix), scale.at(high[*ix].expect("shown"))),
|
|
))
|
|
});
|
|
shown.iter().rev().for_each(|ix| {
|
|
band.line_to(at(
|
|
origin,
|
|
(scene.x(*ix), scale.at(low[*ix].expect("shown"))),
|
|
))
|
|
});
|
|
band.close();
|
|
finish(band, color.opacity(0.08), window);
|
|
reading(
|
|
scene,
|
|
high,
|
|
scale,
|
|
pen.hairline,
|
|
color.opacity(0.6),
|
|
origin,
|
|
window,
|
|
);
|
|
reading(
|
|
scene,
|
|
low,
|
|
scale,
|
|
pen.hairline,
|
|
color.opacity(0.6),
|
|
origin,
|
|
window,
|
|
);
|
|
}
|
|
}
|
|
for (ink, values) in &drawn.lines {
|
|
reading(
|
|
scene,
|
|
values,
|
|
scale,
|
|
pen.stroke * 0.75,
|
|
tint(&pen.palette, *ink),
|
|
origin,
|
|
window,
|
|
);
|
|
}
|
|
}
|
|
if let Some(compare) = &scene.compare {
|
|
reading(
|
|
scene,
|
|
compare,
|
|
scale,
|
|
pen.stroke * 0.75,
|
|
pen.palette.fg_muted,
|
|
origin,
|
|
window,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Each study in its pane: guides, bars about zero, then lines.
|
|
fn studies(scene: &Scene, origin: Point<Pixels>, window: &mut Window) {
|
|
let pen = &scene.pen;
|
|
for ((pane, scale, _), drawn) in scene.panes.iter().skip(1).zip(scene.studies.iter()) {
|
|
for guide in &drawn.guides {
|
|
let y = scale.at(*guide);
|
|
dashed(
|
|
origin,
|
|
((pane.x, y), (pane.x + pane.w, y)),
|
|
pen.palette.border_strong,
|
|
pen,
|
|
window,
|
|
);
|
|
}
|
|
if let Some(bars) = &drawn.bars {
|
|
let (body, zero) = (
|
|
(scene.slot() * 0.6).max(f32::from(pen.hairline)),
|
|
scale.at(0.0),
|
|
);
|
|
for ix in scene.range.clone() {
|
|
let Some(value) = bars[ix] else {
|
|
continue;
|
|
};
|
|
let y = scale.at(value);
|
|
let ink = if value >= 0.0 { pen.rise } else { pen.fall };
|
|
let rect = Rect {
|
|
x: scene.x(ix) - body / 2.0,
|
|
y: y.min(zero),
|
|
w: body,
|
|
h: (y - zero).abs(),
|
|
};
|
|
window.paint_quad(fill(place(origin, rect), ink.opacity(0.45)));
|
|
}
|
|
}
|
|
for (ink, values) in &drawn.lines {
|
|
reading(
|
|
scene,
|
|
values,
|
|
scale,
|
|
pen.stroke * 0.75,
|
|
tint(&pen.palette, *ink),
|
|
origin,
|
|
window,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The market chart's canvas: gridlines, volume and profile, prices, overlays, studies, the last price, alerts, and the crosshair.
|
|
pub(crate) fn drawing(scene: Scene) -> impl IntoElement {
|
|
canvas(
|
|
|_, _, _| {},
|
|
move |bounds, _, window, _| {
|
|
let origin = bounds.origin;
|
|
let pen = &scene.pen;
|
|
for (pane, scale, ticks) in &scene.panes {
|
|
for tick in ticks {
|
|
let y = scale.at(*tick);
|
|
let line = Rect {
|
|
x: pane.x,
|
|
y,
|
|
w: pane.w,
|
|
h: f32::from(pen.hairline),
|
|
};
|
|
window.paint_quad(fill(place(origin, line), pen.palette.border.opacity(0.5)));
|
|
}
|
|
}
|
|
volume(&scene, origin, window);
|
|
let main = scene.panes[0].0;
|
|
window.with_content_mask(
|
|
Some(gpui::ContentMask {
|
|
bounds: place(origin, main),
|
|
}),
|
|
|window| {
|
|
prices(&scene, origin, window);
|
|
overlays(&scene, origin, window);
|
|
tools::drawings(&scene, origin, window);
|
|
},
|
|
);
|
|
studies(&scene, origin, window);
|
|
let edge = main.x + main.w;
|
|
let domain = scene.panes[0].1.domain;
|
|
let shown = |price: &f64| (domain.0..=domain.1).contains(price);
|
|
for price in scene.alerts.iter().filter(|price| shown(price)) {
|
|
let y = scene.panes[0].1.at(*price);
|
|
dashed(
|
|
origin,
|
|
((main.x, y), (edge, y)),
|
|
pen.palette.warning,
|
|
pen,
|
|
window,
|
|
);
|
|
}
|
|
if let Some((price, rose)) = scene.last.filter(|(price, _)| shown(price)) {
|
|
let y = scene.panes[0].1.at(price);
|
|
dashed(
|
|
origin,
|
|
((main.x, y), (edge, y)),
|
|
if rose { pen.rise } else { pen.fall },
|
|
pen,
|
|
window,
|
|
);
|
|
}
|
|
if let Some((ix, row)) = scene.cross {
|
|
let x = scene.visible.x(ix as f64, (main.x, main.w));
|
|
let bottom = scene
|
|
.panes
|
|
.last()
|
|
.map_or(main.y + main.h, |(pane, _, _)| pane.y + pane.h);
|
|
let upright = Rect {
|
|
x,
|
|
y: main.y,
|
|
w: f32::from(pen.hairline),
|
|
h: bottom - main.y,
|
|
};
|
|
window.paint_quad(fill(place(origin, upright), pen.palette.fg_subtle));
|
|
if let Some((pane, y)) = row {
|
|
let across = scene.panes[pane].0;
|
|
dashed(
|
|
origin,
|
|
((across.x, y), (across.x + across.w, y)),
|
|
pen.palette.fg_subtle,
|
|
pen,
|
|
window,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
)
|
|
.absolute()
|
|
.inset_0()
|
|
}
|