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
229 let mut relative_points: Vec<(f32, f32)> = Vec::with_capacity(data.len());
233 for point in data {
234 let x_range = x_scale.scale(point.x);
235 let x_rel = ((x_range - x_min) / x_range_span) as f32;
236 let y_range = y_scale.scale(point.y);
237 let y_rel = if y_min > y_max {
241 (y_range / y_min) as f32
243 } else {
244 ((y_range - y_min) / (y_max - y_min)) as f32
246 };
247 relative_points.push((x_rel, y_rel));
248 }
249
250 let stroke_color = config.stroke_color.to_rgba();
251 let stroke_width = config.stroke_width;
252 let opacity = config.opacity;
253 let curve_type = config.curve;
254 let show_points = config.show_points;
255 let point_radius = config.point_radius;
256 let point_fill = config
257 .point_fill_color
258 .as_ref()
259 .unwrap_or(&config.stroke_color)
260 .to_rgba();
261
262 canvas(
263 move |bounds, _window, _cx| {
265 let width: f32 = bounds.size.width.into();
266 let height: f32 = bounds.size.height.into();
267 let origin_x: f32 = bounds.origin.x.into();
268 let origin_y: f32 = bounds.origin.y.into();
269
270 (relative_points.clone(), width, height, origin_x, origin_y)
271 },
272 move |_bounds,
274 (rel_points, width, height, origin_x, origin_y): (
275 Vec<(f32, f32)>,
276 f32,
277 f32,
278 f32,
279 f32,
280 ),
281 window,
282 _cx| {
283 if rel_points.len() < 2 {
284 return;
285 }
286
287 let segments_to_draw: Vec<(f32, f32, f32, f32)> = match curve_type {
289 CurveType::Linear => {
290 let mut segments = Vec::new();
291 for i in 1..rel_points.len() {
292 let (x0, y0) = rel_points[i - 1];
293 let (x1, y1) = rel_points[i];
294 if let Some(clipped) = clip_line_segment(x0, y0, x1, y1) {
295 segments.push(clipped);
296 }
297 }
298 segments
299 }
300 CurveType::Step | CurveType::StepAfter => {
301 let mut segments = Vec::new();
302 for i in 1..rel_points.len() {
303 let (x0, y0) = rel_points[i - 1];
304 let (x1, y1) = rel_points[i];
305 if let Some(clipped) = clip_line_segment(x0, y0, x1, y0) {
307 segments.push(clipped);
308 }
309 if let Some(clipped) = clip_line_segment(x1, y0, x1, y1) {
310 segments.push(clipped);
311 }
312 }
313 segments
314 }
315 CurveType::StepBefore => {
316 let mut segments = Vec::new();
317 for i in 1..rel_points.len() {
318 let (x0, y0) = rel_points[i - 1];
319 let (x1, y1) = rel_points[i];
320 if let Some(clipped) = clip_line_segment(x0, y0, x0, y1) {
322 segments.push(clipped);
323 }
324 if let Some(clipped) = clip_line_segment(x0, y1, x1, y1) {
325 segments.push(clipped);
326 }
327 }
328 segments
329 }
330 };
331
332 if !segments_to_draw.is_empty() {
334 let mut path_builder = PathBuilder::stroke(px(stroke_width));
335 let mut last_end: Option<(f32, f32)> = None;
336
337 for (x0, y0, x1, y1) in &segments_to_draw {
338 let start = (origin_x + x0 * width, origin_y + y0 * height);
339 let end = (origin_x + x1 * width, origin_y + y1 * height);
340
341 let need_move = match last_end {
343 Some((lx, ly)) => (lx - start.0).abs() > 0.5 || (ly - start.1).abs() > 0.5,
344 None => true,
345 };
346
347 if need_move {
348 path_builder.move_to(gpui::point(px(start.0), px(start.1)));
349 }
350 path_builder.line_to(gpui::point(px(end.0), px(end.1)));
351 last_end = Some(end);
352 }
353
354 if let Ok(path) = path_builder.build() {
355 let color_with_opacity = Rgba {
356 r: stroke_color.r,
357 g: stroke_color.g,
358 b: stroke_color.b,
359 a: stroke_color.a * opacity,
360 };
361 window.paint_path(path, color_with_opacity);
362 }
363 }
364
365 if show_points {
367 for &(x_rel, y_rel) in &rel_points {
368 if (0.0..=1.0).contains(&x_rel) && (0.0..=1.0).contains(&y_rel) {
370 let px_x = origin_x + x_rel * width;
371 let px_y = origin_y + y_rel * height;
372 let point_bounds = Bounds {
373 origin: gpui::point(px(px_x - point_radius), px(px_y - point_radius)),
374 size: gpui::size(px(point_radius * 2.0), px(point_radius * 2.0)),
375 };
376 let color_with_opacity = Rgba {
377 r: point_fill.r,
378 g: point_fill.g,
379 b: point_fill.b,
380 a: point_fill.a * opacity,
381 };
382 window.paint_quad(PaintQuad {
383 bounds: point_bounds,
384 corner_radii: Corners::all(px(point_radius)),
385 background: color_with_opacity.into(),
386 border_widths: Edges::default(),
387 border_color: transparent_black(),
388 border_style: BorderStyle::default(),
389 });
390 }
391 }
392 }
393 },
394 )
395 .size_full()
396 .absolute()
397 .inset_0()
398}