use crate::core::{Affine2D, Bbox, Path, color::Rgba};
use crate::render::{GraphicsContext, Renderer};
use crate::text::FontSource;
use crate::figure::axes::Axes;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct LegendEntry {
pub(crate) color: Rgba,
pub(crate) label: String,
}
mod layout {
pub(super) const FONT_SIZE: f64 = 10.0;
pub(super) const PAD: f64 = 6.0;
pub(super) const SAMPLE_LEN: f64 = 20.0;
pub(super) const SAMPLE_GAP: f64 = 6.0;
pub(super) const ROW_HEIGHT: f64 = 16.0;
pub(super) const MARGIN: f64 = 8.0;
pub(super) const SAMPLE_WIDTH: f64 = 2.0;
pub(super) const BORDER_WIDTH: f64 = 0.8;
}
impl Axes {
pub fn legend(&mut self, entries: Vec<(Rgba, String)>) -> &mut Self {
self.legend = entries
.into_iter()
.map(|(color, label)| LegendEntry { color, label })
.collect();
self
}
pub(crate) fn draw_legend(
&self,
renderer: &mut dyn Renderer,
axes_px: &Bbox,
font: &FontSource,
) {
if self.legend.is_empty() {
return;
}
let label_w = self
.legend
.iter()
.map(|e| font.measure(&e.label, layout::FONT_SIZE).width)
.fold(0.0_f64, f64::max);
let content_w = layout::SAMPLE_LEN + layout::SAMPLE_GAP + label_w;
let box_w = content_w + 2.0 * layout::PAD;
let box_h = self.legend.len() as f64 * layout::ROW_HEIGHT + 2.0 * layout::PAD;
let x1 = axes_px.xmax() - layout::MARGIN;
let y1 = axes_px.ymax() - layout::MARGIN;
let x0 = x1 - box_w;
let y0 = y1 - box_h;
let box_bbox = Bbox::from_extents(x0, y0, x1, y1);
let rect = rect_path(&box_bbox);
let id = Affine2D::identity();
renderer.draw_path(&GraphicsContext::new(), &rect, &id, Some(Rgba::WHITE));
let border_gc = GraphicsContext::new()
.with_stroke(Rgba::rgb(0.5, 0.5, 0.5))
.with_line_width(layout::BORDER_WIDTH);
renderer.draw_path(&border_gc, &rect, &id, None);
for (i, entry) in self.legend.iter().enumerate() {
let row_top = y1 - layout::PAD - i as f64 * layout::ROW_HEIGHT;
let row_mid = row_top - layout::ROW_HEIGHT / 2.0;
let sx0 = x0 + layout::PAD;
let sx1 = sx0 + layout::SAMPLE_LEN;
let sample = Path::from_polyline(&[[sx0, row_mid], [sx1, row_mid]]);
let sample_gc = GraphicsContext::new()
.with_stroke(entry.color)
.with_line_width(layout::SAMPLE_WIDTH);
renderer.draw_path(&sample_gc, &sample, &id, None);
let tx = sx1 + layout::SAMPLE_GAP;
let ty = row_mid - layout::FONT_SIZE / 3.0;
let text = font.text_to_path(&entry.label, layout::FONT_SIZE, [tx, ty]);
renderer.draw_path(&GraphicsContext::new(), &text, &id, Some(Rgba::BLACK));
}
}
}
fn rect_path(bbox: &Bbox) -> Path {
let (x0, y0) = (bbox.xmin(), bbox.ymin());
let (x1, y1) = (bbox.xmax(), bbox.ymax());
Path::from_polyline(&[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]])
}
#[cfg(test)]
mod tests {
use crate::core::color::Rgba;
use crate::figure::Figure;
use crate::skia::SkiaRenderer;
fn pixel(r: &SkiaRenderer, x: u32, y: u32) -> [u8; 4] {
let p = r.pixmap().pixel(x, y).expect("pixel in bounds");
[p.red(), p.green(), p.blue(), p.alpha()]
}
#[test]
fn legend_puts_ink_in_upper_right() {
let mut fig = Figure::new(4.0, 4.0).with_dpi(100.0);
let ax = fig.add_axes(0.1, 0.1, 0.8, 0.8);
ax.plot(&[0.0, 1.0], &[0.0, 1.0]);
ax.legend(vec![
(Rgba::RED, "alpha".to_string()),
(Rgba::BLUE, "beta".to_string()),
]);
let r = fig.render();
let (w, h) = fig.size_px();
let (w, h) = (w as u32, h as u32);
let mut found = false;
'scan: for y in 0..(h / 2) {
for x in (w / 2)..w {
let p = pixel(&r, x, y);
if p != [255, 255, 255, 255] {
found = true;
break 'scan;
}
}
}
assert!(found, "expected legend ink in the upper-right region");
}
}