use std::f32::consts::PI;
use gpui::{
Background, BorderStyle, Bounds, Hsla, PaintQuad, Path, PathBuilder, Pixels, Point, Window,
point, px, quad, size,
};
const HALF_PI: f32 = PI / 2.;
#[allow(clippy::type_complexity)]
pub struct RadialLine<T> {
data: Vec<T>,
angle: Box<dyn Fn(&T, usize) -> Option<f32>>,
radius: Box<dyn Fn(&T, usize) -> Option<f32>>,
closed: bool,
fill: Option<Background>,
stroke: Background,
stroke_width: Pixels,
dot: bool,
dot_size: Pixels,
dot_fill_color: Hsla,
dot_stroke_color: Option<Hsla>,
}
impl<T> Default for RadialLine<T> {
fn default() -> Self {
Self {
data: Vec::new(),
angle: Box::new(|_, _| None),
radius: Box::new(|_, _| None),
closed: false,
fill: None,
stroke: Default::default(),
stroke_width: px(1.),
dot: false,
dot_size: px(4.),
dot_fill_color: gpui::transparent_black(),
dot_stroke_color: None,
}
}
}
impl<T> RadialLine<T> {
pub fn new() -> Self {
Self::default()
}
pub fn data<I>(mut self, data: I) -> Self
where
I: IntoIterator<Item = T>,
{
self.data = data.into_iter().collect();
self
}
pub fn angle<F>(mut self, angle: F) -> Self
where
F: Fn(&T, usize) -> Option<f32> + 'static,
{
self.angle = Box::new(angle);
self
}
pub fn radius<F>(mut self, radius: F) -> Self
where
F: Fn(&T, usize) -> Option<f32> + 'static,
{
self.radius = Box::new(radius);
self
}
pub fn closed(mut self) -> Self {
self.closed = true;
self
}
pub fn fill(mut self, fill: impl Into<Background>) -> Self {
self.fill = Some(fill.into());
self
}
pub fn stroke(mut self, stroke: impl Into<Background>) -> Self {
self.stroke = stroke.into();
self
}
pub fn stroke_width(mut self, stroke_width: impl Into<Pixels>) -> Self {
self.stroke_width = stroke_width.into();
self
}
pub fn dot(mut self) -> Self {
self.dot = true;
self
}
pub fn dot_size(mut self, dot_size: impl Into<Pixels>) -> Self {
self.dot_size = dot_size.into();
self
}
pub fn dot_fill_color(mut self, dot_fill_color: impl Into<Hsla>) -> Self {
self.dot_fill_color = dot_fill_color.into();
self
}
pub fn dot_stroke_color(mut self, dot_stroke_color: impl Into<Hsla>) -> Self {
self.dot_stroke_color = Some(dot_stroke_color.into());
self
}
fn paint_dot(&self, dot: Point<Pixels>) -> PaintQuad {
quad(
gpui::bounds(dot, size(self.dot_size, self.dot_size)),
self.dot_size / 2.,
self.dot_fill_color,
px(1.),
self.dot_stroke_color.unwrap_or(self.dot_fill_color),
BorderStyle::default(),
)
}
fn points(&self, bounds: &Bounds<Pixels>) -> Vec<Point<Pixels>> {
let center_x = bounds.origin.x.as_f32() + bounds.size.width.as_f32() / 2.;
let center_y = bounds.origin.y.as_f32() + bounds.size.height.as_f32() / 2.;
self.data
.iter()
.enumerate()
.filter_map(|(i, v)| {
let angle = (self.angle)(v, i)? - HALF_PI;
let radius = (self.radius)(v, i)?;
Some(point(
px(center_x + radius * angle.cos()),
px(center_y + radius * angle.sin()),
))
})
.collect()
}
fn path(
&self,
bounds: &Bounds<Pixels>,
) -> (Option<Path<Pixels>>, Option<Path<Pixels>>, Vec<PaintQuad>) {
let points = self.points(bounds);
let mut paint_dots = vec![];
if self.dot {
let dot_radius = self.dot_size / 2.;
for p in &points {
paint_dots.push(self.paint_dot(point(p.x - dot_radius, p.y - dot_radius)));
}
}
if points.is_empty() {
return (None, None, paint_dots);
}
let fill_path = self.fill.and_then(|_| {
if points.len() < 3 {
return None;
}
let mut builder = PathBuilder::fill();
builder.add_polygon(&points, true);
builder.build().ok()
});
let mut builder = PathBuilder::stroke(self.stroke_width);
builder.move_to(points[0]);
for p in &points[1..] {
builder.line_to(*p);
}
if self.closed && points.len() > 2 {
builder.close();
}
(fill_path, builder.build().ok(), paint_dots)
}
pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window) {
let (fill_path, stroke_path, dots) = self.path(bounds);
if let (Some(path), Some(fill)) = (fill_path, self.fill) {
window.paint_path(path, fill);
}
if let Some(path) = stroke_path {
window.paint_path(path, self.stroke);
}
for dot in dots {
window.paint_quad(dot);
}
}
}
#[cfg(test)]
mod tests {
use std::f32::consts::TAU;
use super::*;
use gpui::{Bounds, point, px};
#[test]
fn test_radial_line_points() {
let data = vec![1., 1., 1., 1.];
let line = RadialLine::new()
.data(data)
.angle(|_, i| Some(i as f32 * TAU / 4.))
.radius(|v, _| Some(*v * 10.));
let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
let points = line.points(&bounds);
assert_eq!(points.len(), 4);
let expected = [(50., 40.), (60., 50.), (50., 60.), (40., 50.)];
for (p, (x, y)) in points.iter().zip(expected) {
assert!((p.x.as_f32() - x).abs() < 1e-4);
assert!((p.y.as_f32() - y).abs() < 1e-4);
}
}
#[test]
fn test_radial_line_path() {
let data = vec![1., 2., 3.];
let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
let line = RadialLine::new()
.data(data.clone())
.angle(|_, i| Some(i as f32 * TAU / 3.))
.radius(|v, _| Some(*v * 10.));
let (fill_path, stroke_path, dots) = line.path(&bounds);
assert!(fill_path.is_none());
assert!(stroke_path.is_some());
assert!(dots.is_empty());
let line = RadialLine::new()
.data(data)
.angle(|_, i| Some(i as f32 * TAU / 3.))
.radius(|v, _| Some(*v * 10.))
.closed()
.fill(gpui::black())
.dot();
let (fill_path, stroke_path, dots) = line.path(&bounds);
assert!(fill_path.is_some());
assert!(stroke_path.is_some());
assert_eq!(dots.len(), 3);
}
}