use egui::{Color32, Pos2};
use crate::core::items::LineStyle;
use crate::core::transform::{Transform, YAxis};
pub const DEFAULT_MARKER_SIZE: f32 = 8.0;
pub const MARKER_PICK_TOLERANCE_PX: f32 = 5.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MarkerSymbol {
Circle,
Point,
Pixel,
Plus,
Cross,
Diamond,
Square,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum MarkerKind {
Point {
x: f64,
y: f64,
symbol: MarkerSymbol,
size: f32,
},
VLine { x: f64 },
HLine { y: f64 },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum MarkerConstraint {
#[default]
None,
Horizontal,
Vertical,
}
pub fn apply_constraint(
constraint: MarkerConstraint,
from: (f64, f64),
to: (f64, f64),
) -> (f64, f64) {
match constraint {
MarkerConstraint::None => to,
MarkerConstraint::Horizontal => (from.0, to.1),
MarkerConstraint::Vertical => (to.0, from.1),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum TextAnchor {
#[default]
TopLeft,
Top,
TopRight,
Left,
Center,
Right,
BottomLeft,
Bottom,
BottomRight,
}
impl TextAnchor {
pub fn rect_offset(self, size: (f32, f32)) -> (f32, f32) {
let (w, h) = size;
let dx = match self {
TextAnchor::TopLeft | TextAnchor::Left | TextAnchor::BottomLeft => 0.0,
TextAnchor::Top | TextAnchor::Center | TextAnchor::Bottom => -w / 2.0,
TextAnchor::TopRight | TextAnchor::Right | TextAnchor::BottomRight => -w,
};
let dy = match self {
TextAnchor::TopLeft | TextAnchor::Top | TextAnchor::TopRight => 0.0,
TextAnchor::Left | TextAnchor::Center | TextAnchor::Right => -h / 2.0,
TextAnchor::BottomLeft | TextAnchor::Bottom | TextAnchor::BottomRight => -h,
};
(dx, dy)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Marker {
pub kind: MarkerKind,
pub color: Color32,
pub text: Option<String>,
pub bgcolor: Option<Color32>,
pub line_style: LineStyle,
pub line_width: f32,
pub y_axis: YAxis,
pub is_draggable: bool,
pub constraint: MarkerConstraint,
pub text_anchor: TextAnchor,
}
impl Marker {
pub fn point(x: f64, y: f64) -> Self {
Self::with_kind(MarkerKind::Point {
x,
y,
symbol: MarkerSymbol::Circle,
size: DEFAULT_MARKER_SIZE,
})
}
pub fn vline(x: f64) -> Self {
Self::with_kind(MarkerKind::VLine { x })
}
pub fn hline(y: f64) -> Self {
Self::with_kind(MarkerKind::HLine { y })
}
fn with_kind(kind: MarkerKind) -> Self {
Self {
kind,
color: Color32::WHITE,
text: None,
bgcolor: None,
line_style: LineStyle::Solid,
line_width: 1.0,
y_axis: YAxis::Left,
is_draggable: false,
constraint: MarkerConstraint::None,
text_anchor: TextAnchor::TopLeft,
}
}
pub fn with_color(mut self, color: Color32) -> Self {
self.color = color;
self
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
pub fn with_bgcolor(mut self, color: Color32) -> Self {
self.bgcolor = Some(color);
self
}
pub fn with_symbol(mut self, symbol: MarkerSymbol) -> Self {
if let MarkerKind::Point { symbol: s, .. } = &mut self.kind {
*s = symbol;
}
self
}
pub fn with_symbol_size(mut self, size: f32) -> Self {
if let MarkerKind::Point { size: s, .. } = &mut self.kind {
*s = size;
}
self
}
pub fn with_line_style(mut self, style: LineStyle) -> Self {
self.line_style = style;
self
}
pub fn with_line_width(mut self, width: f32) -> Self {
self.line_width = width;
self
}
pub fn with_y_axis(mut self, axis: YAxis) -> Self {
self.y_axis = axis;
self
}
pub fn with_draggable(mut self, draggable: bool) -> Self {
self.is_draggable = draggable;
self
}
pub fn with_constraint(mut self, constraint: MarkerConstraint) -> Self {
self.constraint = constraint;
self
}
pub fn with_text_anchor(mut self, anchor: TextAnchor) -> Self {
self.text_anchor = anchor;
self
}
pub fn position(&self) -> (f64, f64) {
match self.kind {
MarkerKind::Point { x, y, .. } => (x, y),
MarkerKind::VLine { x } => (x, 0.0),
MarkerKind::HLine { y } => (0.0, y),
}
}
pub fn pick(&self, transform: &Transform, cursor: Pos2) -> bool {
let tolerance = MARKER_PICK_TOLERANCE_PX + self.line_width.max(1.0) * 0.5;
match self.kind {
MarkerKind::Point { x, y, size, .. } => {
let radius = size.max(1.0) * 0.5 + MARKER_PICK_TOLERANCE_PX;
transform.data_to_pixel(x, y).distance(cursor) <= radius
}
MarkerKind::VLine { x } => {
let px = transform.data_to_pixel(x, transform.y.min).x;
(cursor.x - px).abs() <= tolerance
&& cursor.y >= transform.area.top() - tolerance
&& cursor.y <= transform.area.bottom() + tolerance
}
MarkerKind::HLine { y } => {
let py = transform.data_to_pixel(transform.x.min, y).y;
(cursor.y - py).abs() <= tolerance
&& cursor.x >= transform.area.left() - tolerance
&& cursor.x <= transform.area.right() + tolerance
}
}
}
pub fn drag(&mut self, from: (f64, f64), to: (f64, f64)) {
if !self.is_draggable {
return;
}
let (fx, fy) = apply_constraint(self.constraint, from, to);
match &mut self.kind {
MarkerKind::Point { x, y, .. } => {
*x = fx;
*y = fy;
}
MarkerKind::VLine { x } => *x = fx,
MarkerKind::HLine { y } => *y = fy,
}
}
pub fn screen_point(&self, t: &Transform) -> Option<Pos2> {
match self.kind {
MarkerKind::Point { x, y, .. } => Some(t.data_to_pixel(x, y)),
_ => None,
}
}
pub fn screen_x(&self, t: &Transform) -> Option<f32> {
match self.kind {
MarkerKind::VLine { x } => Some(t.data_to_pixel(x, t.y.min).x),
_ => None,
}
}
pub fn screen_y(&self, t: &Transform) -> Option<f32> {
match self.kind {
MarkerKind::HLine { y } => Some(t.data_to_pixel(t.x.min, y).y),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use egui::{Rect, pos2};
fn t() -> Transform {
Transform::new(
0.0,
10.0,
0.0,
10.0,
Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)),
)
}
#[test]
fn point_defaults_are_circle_white_left_axis() {
let m = Marker::point(1.0, 2.0);
assert_eq!(
m.kind,
MarkerKind::Point {
x: 1.0,
y: 2.0,
symbol: MarkerSymbol::Circle,
size: DEFAULT_MARKER_SIZE,
}
);
assert_eq!(m.color, Color32::WHITE);
assert_eq!(m.y_axis, YAxis::Left);
assert_eq!(m.line_style, LineStyle::Solid);
assert!(m.text.is_none() && m.bgcolor.is_none());
}
#[test]
fn builders_set_fields() {
let m = Marker::point(0.0, 0.0)
.with_color(Color32::RED)
.with_text("peak")
.with_bgcolor(Color32::BLACK)
.with_symbol(MarkerSymbol::Diamond)
.with_symbol_size(12.0)
.with_y_axis(YAxis::Right);
assert_eq!(m.color, Color32::RED);
assert_eq!(m.text.as_deref(), Some("peak"));
assert_eq!(m.bgcolor, Some(Color32::BLACK));
assert_eq!(m.y_axis, YAxis::Right);
assert_eq!(
m.kind,
MarkerKind::Point {
x: 0.0,
y: 0.0,
symbol: MarkerSymbol::Diamond,
size: 12.0,
}
);
}
#[test]
fn symbol_builders_are_noops_on_line_markers() {
let v = Marker::vline(3.0)
.with_symbol(MarkerSymbol::Square)
.with_symbol_size(20.0);
assert_eq!(v.kind, MarkerKind::VLine { x: 3.0 });
let v = v.with_line_style(LineStyle::Dashed).with_line_width(2.0);
assert_eq!(v.line_style, LineStyle::Dashed);
assert_eq!(v.line_width, 2.0);
}
#[test]
fn constraint_none_moves_both_coordinates() {
let from = (1.0, 2.0);
let to = (5.0, 6.0);
assert_eq!(
apply_constraint(MarkerConstraint::None, from, to),
(5.0, 6.0)
);
}
#[test]
fn horizontal_constraint_pins_x_keeps_y() {
let from = (1.0, 2.0);
let to = (5.0, 6.0);
assert_eq!(
apply_constraint(MarkerConstraint::Horizontal, from, to),
(1.0, 6.0)
);
}
#[test]
fn vertical_constraint_pins_y_keeps_x() {
let from = (1.0, 2.0);
let to = (5.0, 6.0);
assert_eq!(
apply_constraint(MarkerConstraint::Vertical, from, to),
(5.0, 2.0)
);
}
#[test]
fn custom_constraint_closure_applied_directly() {
let snap = |to: (f64, f64)| (to.0.round(), to.1.round());
assert_eq!(snap((5.4, 6.6)), (5.0, 7.0));
}
#[test]
fn drag_free_point_moves_both() {
let mut m = Marker::point(1.0, 2.0).with_draggable(true);
let from = m.position();
m.drag(from, (5.0, 6.0));
assert_eq!(m.position(), (5.0, 6.0));
}
#[test]
fn drag_horizontal_constraint_pins_x() {
let mut m = Marker::point(1.0, 2.0)
.with_draggable(true)
.with_constraint(MarkerConstraint::Horizontal);
let from = m.position();
m.drag(from, (5.0, 6.0));
assert_eq!(m.position(), (1.0, 6.0));
}
#[test]
fn drag_vertical_constraint_pins_y() {
let mut m = Marker::point(1.0, 2.0)
.with_draggable(true)
.with_constraint(MarkerConstraint::Vertical);
let from = m.position();
m.drag(from, (5.0, 6.0));
assert_eq!(m.position(), (5.0, 2.0));
}
#[test]
fn drag_non_draggable_is_a_noop() {
let mut m = Marker::point(1.0, 2.0);
assert!(!m.is_draggable);
let from = m.position();
m.drag(from, (5.0, 6.0));
assert_eq!(m.position(), (1.0, 2.0));
}
#[test]
fn drag_line_markers_update_only_their_axis() {
let mut v = Marker::vline(3.0).with_draggable(true);
let from = v.position();
v.drag(from, (7.0, 99.0));
assert_eq!(v.kind, MarkerKind::VLine { x: 7.0 });
let mut h = Marker::hline(3.0).with_draggable(true);
let from = h.position();
h.drag(from, (99.0, 7.0));
assert_eq!(h.kind, MarkerKind::HLine { y: 7.0 });
}
#[test]
fn text_anchor_default_is_top_left() {
assert_eq!(TextAnchor::default(), TextAnchor::TopLeft);
assert_eq!(Marker::point(0.0, 0.0).text_anchor, TextAnchor::TopLeft);
}
#[test]
fn text_anchor_offsets_against_a_known_rect() {
let size = (40.0, 10.0);
assert_eq!(TextAnchor::TopLeft.rect_offset(size), (0.0, 0.0));
assert_eq!(TextAnchor::TopRight.rect_offset(size), (-40.0, 0.0));
assert_eq!(TextAnchor::BottomLeft.rect_offset(size), (0.0, -10.0));
assert_eq!(TextAnchor::BottomRight.rect_offset(size), (-40.0, -10.0));
assert_eq!(TextAnchor::Top.rect_offset(size), (-20.0, 0.0));
assert_eq!(TextAnchor::Bottom.rect_offset(size), (-20.0, -10.0));
assert_eq!(TextAnchor::Left.rect_offset(size), (0.0, -5.0));
assert_eq!(TextAnchor::Right.rect_offset(size), (-40.0, -5.0));
assert_eq!(TextAnchor::Center.rect_offset(size), (-20.0, -5.0));
}
#[test]
fn pick_point_inside_radius_hits_outside_misses() {
let m = Marker::point(5.0, 5.0).with_symbol_size(10.0);
assert!(m.pick(&t(), pos2(50.0 + 9.0, 50.0)));
assert!(!m.pick(&t(), pos2(50.0 + 12.0, 50.0)));
}
#[test]
fn pick_vline_near_x_within_span_hits_off_span_misses() {
let v = Marker::vline(4.0);
assert!(v.pick(&t(), pos2(43.0, 50.0)));
assert!(!v.pick(&t(), pos2(60.0, 50.0)));
assert!(!v.pick(&t(), pos2(40.0, -50.0)));
}
#[test]
fn pick_hline_near_y_within_span_hits_off_span_misses() {
let h = Marker::hline(8.0);
assert!(h.pick(&t(), pos2(50.0, 23.0)));
assert!(!h.pick(&t(), pos2(50.0, 60.0)));
assert!(!h.pick(&t(), pos2(-50.0, 20.0)));
}
#[test]
fn screen_helpers_select_by_kind() {
let p = Marker::point(2.0, 3.0);
let pos = p.screen_point(&t()).expect("point pos");
assert!((pos.x - 20.0).abs() < 1e-3 && (pos.y - 70.0).abs() < 1e-3);
assert!(p.screen_x(&t()).is_none() && p.screen_y(&t()).is_none());
let v = Marker::vline(4.0);
assert!((v.screen_x(&t()).expect("vline x") - 40.0).abs() < 1e-3);
assert!(v.screen_point(&t()).is_none() && v.screen_y(&t()).is_none());
let h = Marker::hline(8.0);
assert!((h.screen_y(&t()).expect("hline y") - 20.0).abs() < 1e-3);
assert!(h.screen_point(&t()).is_none() && h.screen_x(&t()).is_none());
}
}