use std::sync::Arc;
use super::View;
use super::camera::Area;
use crate::gpu::Inner;
use crate::gpu::shaders as wgsl;
use crate::host::TextRun;
use crate::{Axis, Camera, Color, Format, Plot, Space, Theme};
pub(crate) fn gutters(plot: &Plot, theme: &Theme) -> [f64; 4] {
let f = f64::from(theme.font_size);
let top = if plot.y.label.is_some() && !plot.y.hidden { f * 2.4 } else { f };
let bottom = if plot.x.hidden { 8.0 } else { f * 2.4 + if plot.x.label.is_some() { f * 2.2 } else { 0.0 } };
let left = if plot.y.hidden { 8.0 } else { f * 6.0 };
[top, f * 1.5, bottom, left]
}
pub(super) fn step(span: f64, count: f64) -> f64 {
let raw = (span / count.max(1.0)).max(f64::MIN_POSITIVE);
let power = 10_f64.powf(raw.log10().floor());
[1.0, 2.0, 2.5, 5.0, 10.0].into_iter().map(|m| m * power).find(|&s| s >= raw).unwrap_or(10.0 * power)
}
fn decimals(step: f64) -> usize {
let magnitude = step.log10().floor();
let extra = usize::from(((step / 10_f64.powf(magnitude)) - 2.5).abs() < 1e-9);
(-magnitude).max(0.0) as usize + extra
}
pub(super) fn label(value: f64, step: f64, axis: &Axis) -> String {
let value = if value.abs() < step * 1e-9 { 0.0 } else { value };
let precision = axis.precision.map(usize::from);
let significand = precision.unwrap_or(2);
match axis.format {
Format::Auto if value != 0.0 && (value.abs() >= 1e6 || value.abs() < 1e-4) => format!("{value:.significand$e}"),
Format::Auto | Format::Fixed => {
let decimals = precision.unwrap_or_else(|| decimals(step));
format!("{value:.decimals$}")
}
Format::Scientific => format!("{value:.significand$e}"),
Format::Engineering => {
if value == 0.0 {
return format!("{value:.significand$}");
}
let power = (value.abs().log10() / 3.0).floor() as i32 * 3;
let mantissa = value / 10_f64.powi(power);
if power == 0 { format!("{mantissa:.significand$}") } else { format!("{mantissa:.significand$}e{power}") }
}
}
}
fn axis_ticks(axis: &Axis, range: [f64; 2], step: f64) -> Vec<(f64, String)> {
match &axis.ticks {
Some(given) => given
.iter()
.filter(|(v, _)| (range[0]..=range[1]).contains(v))
.map(|(v, text)| (*v, text.clone().unwrap_or_else(|| label(*v, step, axis))))
.collect(),
None => ticks(range, step).map(|v| (v, label(v, step, axis))).collect(),
}
}
pub(super) fn ticks(range: [f64; 2], step: f64) -> impl Iterator<Item = f64> {
let first = (range[0] / step).ceil();
let last = (range[1] / step).floor().min(first + 200.0);
let count = if first.is_finite() && last >= first { (last - first) as u32 + 1 } else { 0 };
(0..count).map(move |i| (first + f64::from(i)) * step)
}
pub(super) fn line(a: [f64; 2], b: [f64; 2], width: f32, color: Color) -> wgsl::Sprite {
let mut sprite: wgsl::Sprite = bytemuck::Zeroable::zeroed();
sprite.a = [a[0] as f32, a[1] as f32];
sprite.b = [b[0] as f32, b[1] as f32];
sprite.width = width;
sprite.color = color.to_array();
sprite.kind = wgsl::SPRITE_LINE;
sprite
}
#[derive(Clone, Copy)]
pub(super) enum Align {
Below,
Left,
Right,
Above,
BelowLeft,
BelowRight,
}
pub(super) struct Writer<'a> {
pub(super) inner: &'a mut Inner,
pub(super) font: Arc<str>,
pub(super) size_px: u32,
pub(super) ratio: f64,
pub(super) color: Color,
}
impl Writer<'_> {
pub(super) fn text(&mut self, text: &str, anchor: [f64; 2], align: Align) -> Option<wgsl::Sprite> {
let run = TextRun { text: text.into(), font: self.font.clone(), size_px: self.size_px, max_width: None };
let host = self.inner.host.clone();
let glyphs = self.inner.atlas.text(host.as_deref(), &run)?;
let [l, t, r, b] = glyphs.rect.map(f64::from);
let (w, h) = ((r - l) / self.ratio, (b - t) / self.ratio);
let [x, y] = match align {
Align::Below => [anchor[0] - w / 2.0, anchor[1]],
Align::Left => [anchor[0] - w, anchor[1] - h / 2.0],
Align::Right => [anchor[0], anchor[1] - h / 2.0],
Align::Above => [anchor[0], anchor[1] - h],
Align::BelowLeft => [anchor[0] - w, anchor[1]],
Align::BelowRight => anchor,
};
let snap = |v: f64| (v * self.ratio).round() / self.ratio;
let (x, y) = (snap(x), snap(y));
let mut sprite: wgsl::Sprite = bytemuck::Zeroable::zeroed();
sprite.a = [x as f32, y as f32];
sprite.b = [(x + w) as f32, (y + h) as f32];
sprite.uv = [l as f32, t as f32, r as f32, b as f32];
sprite.color = self.color.to_array();
sprite.kind = wgsl::SPRITE_TEXT;
Some(sprite)
}
}
impl View {
pub(super) fn overlay(&self, inner: &mut Inner) -> Vec<wgsl::Sprite> {
let evictions = inner.atlas.evictions;
let mut sprites = self.written(inner);
if inner.atlas.evictions != evictions {
sprites = self.written(inner);
}
let theme = &self.scene.theme;
let color = theme.selected.unwrap_or(theme.hover);
let at = |p: [f32; 2]| [f64::from(p[0]), f64::from(p[1])];
match self.gestures.now {
super::gestures::Gesture::Box { from, to, .. } => {
let mut sprite = line(at(from), at(to), 1.0, color);
sprite.kind = wgsl::SPRITE_BOX;
sprites.push(sprite);
}
super::gestures::Gesture::Connect { anchor: Some(from), to, .. } => {
sprites.push(line(at(from), at(to), 2.0, color));
}
_ => {}
}
sprites
}
fn written(&self, inner: &mut Inner) -> Vec<wgsl::Sprite> {
let mut sprites = self.axes_sprites(inner);
sprites.extend(self.annotations(inner));
sprites
}
fn axes_sprites(&self, inner: &mut Inner) -> Vec<wgsl::Sprite> {
let Space::Plot(plot) = &self.scene.space else { return Vec::new() };
let Camera::Plot { x, y: Some(y) } = self.drawn_camera() else { return Vec::new() };
self.axes(inner, plot, &self.scene.theme, self.area(), x, y)
}
fn axes(
&self,
inner: &mut Inner,
plot: &Plot,
theme: &Theme,
area: Area,
x: [f64; 2],
y: [f64; 2],
) -> Vec<wgsl::Sprite> {
let [left, top, right, bottom] = area;
let across = |v: f64| left + (v - x[0]) / (x[1] - x[0]) * (right - left);
let up = |v: f64| bottom - (v - y[0]) / (y[1] - y[0]) * (bottom - top);
let font = f64::from(theme.font_size);
let ratio = f64::from(self.pixel_ratio);
let mut writer = Writer {
inner,
font: theme.font.clone(),
size_px: (font * ratio).round().max(1.0) as u32,
ratio,
color: theme.text,
};
let axis = theme.text.alpha(0.35);
let mut sprites = Vec::new();
if !plot.x.hidden {
let step = step(x[1] - x[0], (right - left) / (font * 8.0));
for (v, text) in axis_ticks(&plot.x, x, step) {
let at = across(v);
if plot.x.grid {
sprites.push(line([at, top], [at, bottom], 1.0, theme.grid));
}
sprites.push(line([at, bottom], [at, bottom + 4.0], 1.0, axis));
sprites.extend(writer.text(&text, [at, bottom + 6.0], Align::Below));
}
sprites.push(line([left, bottom], [right, bottom], 1.0, axis));
if let Some(title) = &plot.x.label {
sprites.extend(writer.text(title, [right, bottom + font * 2.2], Align::BelowLeft));
}
}
if !plot.y.hidden {
let step = step(y[1] - y[0], (bottom - top) / (font * 3.5));
for (v, text) in axis_ticks(&plot.y, y, step) {
let at = up(v);
if plot.y.grid {
sprites.push(line([left, at], [right, at], 1.0, theme.grid));
}
sprites.push(line([left - 4.0, at], [left, at], 1.0, axis));
sprites.extend(writer.text(&text, [left - 7.0, at], Align::Left));
}
sprites.push(line([left, top], [left, bottom], 1.0, axis));
if let Some(title) = &plot.y.label {
sprites.extend(writer.text(title, [left - font * 4.0, top - font * 0.8], Align::Above));
}
}
if self.at.is_finite() && (x[0]..=x[1]).contains(&self.at) {
let at = across(self.at);
sprites.push(line([at, top], [at, bottom], 1.5, theme.playhead));
}
sprites
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn steps_are_round_and_labels_fit_them() {
assert!((step(12.0, 6.0) - 2.0).abs() < 1e-12);
assert!((step(1.0, 4.0) - 0.25).abs() < 1e-12);
let auto = Axis::default();
assert_eq!(label(0.25, 0.25, &auto), "0.25");
assert_eq!(label(4.0, 2.0, &auto), "4");
assert_eq!(label(-1e-17, 0.5, &auto), "0.0");
let with = |format, precision| Axis { format, precision, ..Axis::default() };
assert_eq!(label(1500.0, 500.0, &with(Format::Scientific, Some(1))), "1.5e3");
assert_eq!(label(0.00015, 0.00005, &with(Format::Engineering, Some(0))), "150e-6");
assert_eq!(label(2.0, 1.0, &with(Format::Fixed, Some(2))), "2.00");
let given = Axis { ticks: Some(vec![(0.0, Some("off".into())), (1.0, None), (5.0, None)]), ..Axis::default() };
assert_eq!(axis_ticks(&given, [0.0, 2.0], 1.0), [(0.0, "off".to_owned()), (1.0, "1".to_owned())]);
assert_eq!(ticks([0.1, 1.0], 0.25).count(), 4);
}
}