use egui::{Color32, Pos2};
use crate::core::items::{LineStyle, Symbol};
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)]
pub enum MarkerKind {
Point {
x: f64,
y: f64,
symbol: Symbol,
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)
}
pub fn pixel_offset(self, padding: (f32, f32)) -> (f32, f32) {
let (px, py) = padding;
let dx = match self {
TextAnchor::TopLeft | TextAnchor::Left | TextAnchor::BottomLeft => px,
TextAnchor::TopRight | TextAnchor::Right | TextAnchor::BottomRight => -px,
TextAnchor::Top | TextAnchor::Center | TextAnchor::Bottom => 0.0,
};
let dy = match self {
TextAnchor::TopLeft | TextAnchor::Top | TextAnchor::TopRight => py,
TextAnchor::BottomLeft | TextAnchor::Bottom | TextAnchor::BottomRight => -py,
TextAnchor::Left | TextAnchor::Center | TextAnchor::Right => 0.0,
};
(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: Symbol::Circle,
size: DEFAULT_MARKER_SIZE,
})
}
pub fn vline(x: f64) -> Self {
Self::with_kind(MarkerKind::VLine { x })
}
pub fn hline(y: f64) -> Self {
Self {
text_anchor: TextAnchor::TopRight,
..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: Symbol) -> 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)) {
let constraint = self.constraint;
self.drag_with(to, |x, y| apply_constraint(constraint, from, (x, y)));
}
pub fn drag_with(&mut self, to: (f64, f64), constraint: impl Fn(f64, f64) -> (f64, f64)) {
if !self.is_draggable {
return;
}
let (fx, fy) = constraint(to.0, to.1);
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: Symbol::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(Symbol::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: Symbol::Diamond,
size: 12.0,
}
);
}
#[test]
fn symbol_builders_are_noops_on_line_markers() {
let v = Marker::vline(3.0)
.with_symbol(Symbol::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 drag_with_custom_constraint_filters_a_point_target() {
let mut m = Marker::point(1.0, 2.0).with_draggable(true);
m.drag_with((5.4, 6.6), |x, y| (x.round(), y.round()));
assert_eq!(m.position(), (5.0, 7.0));
}
#[test]
fn drag_with_updates_only_the_line_marker_axis() {
let mut v = Marker::vline(1.0).with_draggable(true);
v.drag_with((9.0, 3.0), |x, y| (x.round(), y.round()));
assert_eq!(v.kind, MarkerKind::VLine { x: 9.0 });
let mut h = Marker::hline(1.0).with_draggable(true);
h.drag_with((9.0, 3.0), |x, y| (x.round(), y.round()));
assert_eq!(h.kind, MarkerKind::HLine { y: 3.0 });
}
#[test]
fn drag_with_is_a_noop_when_not_draggable() {
let mut m = Marker::point(1.0, 2.0);
m.drag_with((5.0, 6.0), |x, y| (x, y));
assert_eq!(m.position(), (1.0, 2.0));
}
#[test]
fn drag_delegates_to_drag_with_via_enum_presets() {
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_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_per_kind_defaults_match_silx() {
assert_eq!(Marker::point(1.0, 2.0).text_anchor, TextAnchor::TopLeft);
assert_eq!(Marker::vline(1.0).text_anchor, TextAnchor::TopLeft);
assert_eq!(Marker::hline(2.0).text_anchor, TextAnchor::TopRight);
}
#[test]
fn pixel_offset_applies_silx_alignment_signs() {
assert_eq!(TextAnchor::TopLeft.pixel_offset((10.0, 3.0)), (10.0, 3.0));
assert_eq!(TextAnchor::TopRight.pixel_offset((5.0, 3.0)), (-5.0, 3.0));
assert_eq!(TextAnchor::BottomLeft.pixel_offset((5.0, 3.0)), (5.0, -3.0));
assert_eq!(TextAnchor::Top.pixel_offset((5.0, 3.0)), (0.0, 3.0));
assert_eq!(TextAnchor::Left.pixel_offset((5.0, 3.0)), (5.0, 0.0));
assert_eq!(TextAnchor::Center.pixel_offset((5.0, 3.0)), (0.0, 0.0));
assert_eq!(
TextAnchor::BottomRight.pixel_offset((5.0, 3.0)),
(-5.0, -3.0)
);
}
#[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());
}
}