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)>, pub visible: Visible, pub range: Range, pub candles: Rc>, pub kind: ChartKind, pub volume: Option, pub profile: Vec<(f64, f64, f64)>, pub overlays: Rc>, pub studies: Rc>, pub compare: Option>>, pub last: Option<(f64, bool)>, pub alerts: Vec, pub cross: Option<(i64, Option<(usize, f32)>)>, pub drawings: Vec, 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, (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], scale: &Linear, width: Pixels, color: Hsla, origin: Point, 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, 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> = 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, 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, 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 = 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, 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() }