1use crate::color::D3Color;
4use crate::scale::Scale;
5use gpui::prelude::*;
6use gpui::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum CurveType {
11 Linear,
13 Step,
15 StepBefore,
17 StepAfter,
19}
20
21#[derive(Clone)]
23pub struct LineConfig {
24 pub stroke_color: D3Color,
26 pub stroke_width: f32,
28 pub opacity: f32,
30 pub curve: CurveType,
32 pub show_points: bool,
34 pub point_radius: f32,
36 pub point_fill_color: Option<D3Color>,
38}
39
40impl Default for LineConfig {
41 fn default() -> Self {
42 Self {
43 stroke_color: D3Color::from_hex(0x4682b4), stroke_width: 2.0,
45 opacity: 1.0,
46 curve: CurveType::Linear,
47 show_points: false,
48 point_radius: 3.0,
49 point_fill_color: None,
50 }
51 }
52}
53
54impl LineConfig {
55 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn stroke_color(mut self, color: D3Color) -> Self {
62 self.stroke_color = color;
63 self
64 }
65
66 pub fn stroke_width(mut self, width: f32) -> Self {
68 self.stroke_width = width;
69 self
70 }
71
72 pub fn opacity(mut self, opacity: f32) -> Self {
74 self.opacity = opacity.clamp(0.0, 1.0);
75 self
76 }
77
78 pub fn curve(mut self, curve: CurveType) -> Self {
80 self.curve = curve;
81 self
82 }
83
84 pub fn show_points(mut self, show: bool) -> Self {
86 self.show_points = show;
87 self
88 }
89
90 pub fn point_radius(mut self, radius: f32) -> Self {
92 self.point_radius = radius;
93 self
94 }
95
96 pub fn point_fill_color(mut self, color: D3Color) -> Self {
98 self.point_fill_color = Some(color);
99 self
100 }
101}
102
103#[derive(Debug, Clone, Copy)]
105pub struct LinePoint {
106 pub x: f64,
108 pub y: f64,
110}
111
112impl LinePoint {
113 pub fn new(x: f64, y: f64) -> Self {
115 Self { x, y }
116 }
117}
118
119fn clip_line_segment(x0: f32, y0: f32, x1: f32, y1: f32) -> Option<(f32, f32, f32, f32)> {
122 const INSIDE: u8 = 0;
123 const LEFT: u8 = 1;
124 const RIGHT: u8 = 2;
125 const BOTTOM: u8 = 4;
126 const TOP: u8 = 8;
127
128 fn compute_outcode(x: f32, y: f32) -> u8 {
129 let mut code = INSIDE;
130 if x < 0.0 {
131 code |= LEFT;
132 } else if x > 1.0 {
133 code |= RIGHT;
134 }
135 if y < 0.0 {
136 code |= TOP;
137 } else if y > 1.0 {
138 code |= BOTTOM;
139 }
140 code
141 }
142
143 let mut x0 = x0;
144 let mut y0 = y0;
145 let mut x1 = x1;
146 let mut y1 = y1;
147 let mut outcode0 = compute_outcode(x0, y0);
148 let mut outcode1 = compute_outcode(x1, y1);
149
150 loop {
151 if (outcode0 | outcode1) == 0 {
152 return Some((x0, y0, x1, y1));
154 } else if (outcode0 & outcode1) != 0 {
155 return None;
157 } else {
158 let outcode_out = if outcode0 != 0 { outcode0 } else { outcode1 };
160 let (x, y);
161
162 if (outcode_out & TOP) != 0 {
163 x = x0 + (x1 - x0) * (0.0 - y0) / (y1 - y0);
164 y = 0.0;
165 } else if (outcode_out & BOTTOM) != 0 {
166 x = x0 + (x1 - x0) * (1.0 - y0) / (y1 - y0);
167 y = 1.0;
168 } else if (outcode_out & RIGHT) != 0 {
169 y = y0 + (y1 - y0) * (1.0 - x0) / (x1 - x0);
170 x = 1.0;
171 } else {
172 y = y0 + (y1 - y0) * (0.0 - x0) / (x1 - x0);
174 x = 0.0;
175 }
176
177 if outcode_out == outcode0 {
178 x0 = x;
179 y0 = y;
180 outcode0 = compute_outcode(x0, y0);
181 } else {
182 x1 = x;
183 y1 = y;
184 outcode1 = compute_outcode(x1, y1);
185 }
186 }
187 }
188}
189
190pub fn render_line<XS, YS>(
216 x_scale: &XS,
217 y_scale: &YS,
218 data: &[LinePoint],
219 config: &LineConfig,
220) -> impl IntoElement
221where
222 XS: Scale<f64, f64>,
223 YS: Scale<f64, f64>,
224{
225 let (x_min, x_max) = x_scale.range();
226 let (y_min, y_max) = y_scale.range();
227 let x_range_span = x_max - x_min;
228 let y_range_span = (y_max - y_min).abs();
229
230 let mut relative_points: Vec<(f32, f32)> = Vec::with_capacity(data.len());
232 for point in data {
233 let x_range = x_scale.scale(point.x);
234 let x_rel = ((x_range - x_min) / x_range_span) as f32;
235 let y_range = y_scale.scale(point.y);
236 let y_rel = 1.0 - ((y_range - y_min) / y_range_span) as f32;
238 relative_points.push((x_rel, y_rel));
239 }
240
241 let stroke_color = config.stroke_color.to_rgba();
242 let stroke_width = config.stroke_width;
243 let opacity = config.opacity;
244 let curve_type = config.curve;
245 let show_points = config.show_points;
246 let point_radius = config.point_radius;
247 let point_fill = config
248 .point_fill_color
249 .as_ref()
250 .unwrap_or(&config.stroke_color)
251 .to_rgba();
252
253 canvas(
254 move |bounds, _window, _cx| {
256 let width: f32 = bounds.size.width.into();
257 let height: f32 = bounds.size.height.into();
258 let origin_x: f32 = bounds.origin.x.into();
259 let origin_y: f32 = bounds.origin.y.into();
260
261 (relative_points.clone(), width, height, origin_x, origin_y)
262 },
263 move |_bounds,
265 (rel_points, width, height, origin_x, origin_y): (
266 Vec<(f32, f32)>,
267 f32,
268 f32,
269 f32,
270 f32,
271 ),
272 window,
273 _cx| {
274 if rel_points.len() < 2 {
275 return;
276 }
277
278 let segments_to_draw: Vec<(f32, f32, f32, f32)> = match curve_type {
280 CurveType::Linear => {
281 let mut segments = Vec::new();
282 for i in 1..rel_points.len() {
283 let (x0, y0) = rel_points[i - 1];
284 let (x1, y1) = rel_points[i];
285 if let Some(clipped) = clip_line_segment(x0, y0, x1, y1) {
286 segments.push(clipped);
287 }
288 }
289 segments
290 }
291 CurveType::Step | CurveType::StepAfter => {
292 let mut segments = Vec::new();
293 for i in 1..rel_points.len() {
294 let (x0, y0) = rel_points[i - 1];
295 let (x1, y1) = rel_points[i];
296 if let Some(clipped) = clip_line_segment(x0, y0, x1, y0) {
298 segments.push(clipped);
299 }
300 if let Some(clipped) = clip_line_segment(x1, y0, x1, y1) {
301 segments.push(clipped);
302 }
303 }
304 segments
305 }
306 CurveType::StepBefore => {
307 let mut segments = Vec::new();
308 for i in 1..rel_points.len() {
309 let (x0, y0) = rel_points[i - 1];
310 let (x1, y1) = rel_points[i];
311 if let Some(clipped) = clip_line_segment(x0, y0, x0, y1) {
313 segments.push(clipped);
314 }
315 if let Some(clipped) = clip_line_segment(x0, y1, x1, y1) {
316 segments.push(clipped);
317 }
318 }
319 segments
320 }
321 };
322
323 if !segments_to_draw.is_empty() {
325 let mut path_builder = PathBuilder::stroke(px(stroke_width));
326 let mut last_end: Option<(f32, f32)> = None;
327
328 for (x0, y0, x1, y1) in &segments_to_draw {
329 let start = (origin_x + x0 * width, origin_y + y0 * height);
330 let end = (origin_x + x1 * width, origin_y + y1 * height);
331
332 let need_move = match last_end {
334 Some((lx, ly)) => (lx - start.0).abs() > 0.5 || (ly - start.1).abs() > 0.5,
335 None => true,
336 };
337
338 if need_move {
339 path_builder.move_to(gpui::point(px(start.0), px(start.1)));
340 }
341 path_builder.line_to(gpui::point(px(end.0), px(end.1)));
342 last_end = Some(end);
343 }
344
345 if let Ok(path) = path_builder.build() {
346 let color_with_opacity = Rgba {
347 r: stroke_color.r,
348 g: stroke_color.g,
349 b: stroke_color.b,
350 a: stroke_color.a * opacity,
351 };
352 window.paint_path(path, color_with_opacity);
353 }
354 }
355
356 if show_points {
358 for &(x_rel, y_rel) in &rel_points {
359 if x_rel >= 0.0 && x_rel <= 1.0 && y_rel >= 0.0 && y_rel <= 1.0 {
361 let px_x = origin_x + x_rel * width;
362 let px_y = origin_y + y_rel * height;
363 let point_bounds = Bounds {
364 origin: gpui::point(px(px_x - point_radius), px(px_y - point_radius)),
365 size: gpui::size(px(point_radius * 2.0), px(point_radius * 2.0)),
366 };
367 let color_with_opacity = Rgba {
368 r: point_fill.r,
369 g: point_fill.g,
370 b: point_fill.b,
371 a: point_fill.a * opacity,
372 };
373 window.paint_quad(PaintQuad {
374 bounds: point_bounds,
375 corner_radii: Corners::all(px(point_radius)),
376 background: color_with_opacity.into(),
377 border_widths: Edges::default(),
378 border_color: transparent_black(),
379 border_style: BorderStyle::default(),
380 });
381 }
382 }
383 }
384 },
385 )
386 .size_full()
387 .absolute()
388 .inset_0()
389}