use gpui::{Bounds, Hsla, Pixels, Point, Window, fill, point, px, size};
use super::origin_point;
pub struct Grid {
x: Vec<Pixels>,
y: Vec<Pixels>,
stroke: Hsla,
dash_array: Option<Vec<Pixels>>,
}
impl Grid {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
x: vec![],
y: vec![],
stroke: Default::default(),
dash_array: None,
}
}
pub fn x(mut self, x: Vec<impl Into<Pixels>>) -> Self {
self.x = x.into_iter().map(|v| v.into()).collect();
self
}
pub fn y(mut self, y: Vec<impl Into<Pixels>>) -> Self {
self.y = y.into_iter().map(|v| v.into()).collect();
self
}
pub fn stroke(mut self, stroke: impl Into<Hsla>) -> Self {
self.stroke = stroke.into();
self
}
pub fn dash_array(mut self, dash_array: &[Pixels]) -> Self {
self.dash_array = Some(dash_array.to_vec());
self
}
fn points(&self, bounds: &Bounds<Pixels>) -> Vec<(Point<Pixels>, Point<Pixels>)> {
let size = bounds.size;
let origin = bounds.origin;
let mut x = self
.x
.iter()
.map(|x| {
(
origin_point(*x, px(0.), origin),
origin_point(*x, size.height, origin),
)
})
.collect::<Vec<_>>();
let y = self
.y
.iter()
.map(|y| {
(
origin_point(px(0.), *y, origin),
origin_point(size.width, *y, origin),
)
})
.collect::<Vec<_>>();
x.extend(y);
x
}
pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window) {
for (start, end) in self.points(bounds) {
for (start, end) in dash_segments(start, end, self.dash_array.as_deref()) {
window.paint_quad(fill(line_bounds(start, end), self.stroke));
}
}
}
}
fn line_bounds(start: Point<Pixels>, end: Point<Pixels>) -> Bounds<Pixels> {
let half = px(0.5);
if start.x == end.x {
let top = start.y.min(end.y);
Bounds::new(
point(start.x - half, top),
size(px(1.), start.y.max(end.y) - top),
)
} else {
let left = start.x.min(end.x);
Bounds::new(
point(left, start.y - half),
size(start.x.max(end.x) - left, px(1.)),
)
}
}
fn dash_segments(
start: Point<Pixels>,
end: Point<Pixels>,
dash_array: Option<&[Pixels]>,
) -> Vec<(Point<Pixels>, Point<Pixels>)> {
let Some(dash_array) = dash_array.filter(|dashes| !dashes.is_empty()) else {
return vec![(start, end)];
};
let length = ((end.x - start.x).as_f32().powi(2) + (end.y - start.y).as_f32().powi(2)).sqrt();
if length <= 0. || dash_array.iter().all(|dash| dash.as_f32() <= 0.) {
return vec![(start, end)];
}
let at = |distance: f32| {
let t = distance / length;
point(
start.x + (end.x - start.x) * t,
start.y + (end.y - start.y) * t,
)
};
let pattern_len = if dash_array.len() % 2 == 1 {
dash_array.len() * 2
} else {
dash_array.len()
};
let mut segments = Vec::new();
let mut position = 0.;
let mut index = 0;
while position < length {
let dash = dash_array[index % dash_array.len()].as_f32().max(0.);
let next = (position + dash).min(length);
if index % 2 == 0 && next > position {
segments.push((at(position), at(next)));
}
position = next;
index = (index + 1) % pattern_len;
}
segments
}
#[cfg(test)]
mod tests {
use super::*;
fn xs(segments: &[(Point<Pixels>, Point<Pixels>)]) -> Vec<(f32, f32)> {
segments
.iter()
.map(|(start, end)| (start.x.as_f32(), end.x.as_f32()))
.collect()
}
#[test]
fn solid_line_is_one_segment() {
let segments = dash_segments(point(px(0.), px(5.)), point(px(10.), px(5.)), None);
assert_eq!(xs(&segments), vec![(0., 10.)]);
let segments = dash_segments(point(px(0.), px(5.)), point(px(10.), px(5.)), Some(&[]));
assert_eq!(xs(&segments), vec![(0., 10.)]);
}
#[test]
fn dashes_alternate_and_clip_at_the_end() {
let segments = dash_segments(
point(px(0.), px(5.)),
point(px(11.), px(5.)),
Some(&[px(4.), px(2.)]),
);
assert_eq!(xs(&segments), vec![(0., 4.), (6., 10.)]);
}
#[test]
fn odd_dash_array_repeats_like_svg() {
let segments = dash_segments(
point(px(0.), px(0.)),
point(px(0.), px(20.)),
Some(&[px(5.), px(3.), px(2.)]),
);
let ys: Vec<_> = segments
.iter()
.map(|(start, end)| (start.y.as_f32(), end.y.as_f32()))
.collect();
assert_eq!(ys, vec![(0., 5.), (8., 10.), (15., 18.)]);
}
#[test]
fn line_box_is_one_pixel_centred_on_the_coordinate() {
let vertical = line_bounds(point(px(10.), px(0.)), point(px(10.), px(40.)));
assert_eq!(vertical.origin, point(px(9.5), px(0.)));
assert_eq!(vertical.size, size(px(1.), px(40.)));
let horizontal = line_bounds(point(px(40.), px(7.)), point(px(0.), px(7.)));
assert_eq!(horizontal.origin, point(px(0.), px(6.5)));
assert_eq!(horizontal.size, size(px(40.), px(1.)));
}
}