use crate::core::{Color, HorizontalAlignment, Point, Rect};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CrosshairMode {
Vertical,
Horizontal,
#[default]
Both,
}
impl CrosshairMode {
pub fn as_str(self) -> &'static str {
match self {
CrosshairMode::Vertical => "vertical",
CrosshairMode::Horizontal => "horizontal",
CrosshairMode::Both => "both",
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name {
"vertical" => Some(CrosshairMode::Vertical),
"horizontal" => Some(CrosshairMode::Horizontal),
"both" => Some(CrosshairMode::Both),
_ => None,
}
}
}
pub struct ChartCrosshair {
base: BaseWidget,
enabled: bool,
mode: CrosshairMode,
snap_to_point: bool,
line_color: Option<Color>,
line_width: u32,
show_axis_values: bool,
show_marker: bool,
point: Option<Point>,
data: Option<(f64, f64)>,
pub crosshair_moved: Signal1<Option<(f64, f64)>>,
}
impl ChartCrosshair {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Panel, geometry, "ChartCrosshair"),
enabled: true,
mode: CrosshairMode::Both,
snap_to_point: false,
line_color: None,
line_width: 1,
show_axis_values: true,
show_marker: true,
point: None,
data: None,
crosshair_moved: Signal1::new(),
}
}
pub fn show_at(&mut self, point: Point) {
self.point = Some(point);
if let Some(data) = self.data {
self.crosshair_moved.emit(Some(data));
}
self.base.request_redraw();
}
pub fn show_at_data(&mut self, point: Point, data: (f64, f64)) {
self.enabled = true;
self.point = Some(point);
self.data = Some(data);
self.crosshair_moved.emit(Some(data));
self.base.request_redraw();
}
pub fn hide(&mut self) {
if self.point.take().is_none() && self.data.take().is_none() {
return;
}
self.data = None;
self.crosshair_moved.emit(None);
self.base.request_redraw();
}
pub fn is_visible(&self) -> bool {
self.point.is_some()
}
pub fn enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
if !enabled {
self.hide();
}
}
pub fn mode(&self) -> CrosshairMode {
self.mode
}
pub fn set_mode(&mut self, mode: CrosshairMode) {
self.mode = mode;
self.base.request_redraw();
}
pub fn snap_to_point(&self) -> bool {
self.snap_to_point
}
pub fn set_snap_to_point(&mut self, snap: bool) {
self.snap_to_point = snap;
}
pub fn line_color(&self) -> Option<Color> {
self.line_color
}
pub fn set_line_color(&mut self, color: Option<Color>) {
self.line_color = color;
self.base.request_redraw();
}
pub fn line_width(&self) -> u32 {
self.line_width
}
pub fn set_line_width(&mut self, width: u32) {
self.line_width = width.max(1);
self.base.request_redraw();
}
pub fn show_axis_values(&self) -> bool {
self.show_axis_values
}
pub fn set_show_axis_values(&mut self, show: bool) {
self.show_axis_values = show;
self.base.request_redraw();
}
pub fn show_marker(&self) -> bool {
self.show_marker
}
pub fn set_show_marker(&mut self, show: bool) {
self.show_marker = show;
self.base.request_redraw();
}
pub fn data_value(&self) -> Option<(f64, f64)> {
self.data
}
fn resolved_color(&self) -> Color {
self.line_color.unwrap_or_else(|| {
crate::style::palette_shared()
.as_ref()
.map(|colors| colors.outline)
.unwrap_or(Color::rgba(128, 128, 128, 180))
})
}
}
impl Widget for ChartCrosshair {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(120, 120)
}
crate::impl_draw_bridge!();
}
impl Draw for ChartCrosshair {
fn draw(&mut self, context: &mut RenderContext) {
let Some(point) = self.point else {
return;
};
if !self.enabled {
return;
}
let rect = self.base.geometry();
if rect.width == 0 || rect.height == 0 {
return;
}
let color = self.resolved_color();
let width = self.line_width.max(1);
let left = rect.x.max(0);
let top = rect.y.max(0);
let right = rect.x.saturating_add(rect.width as i32);
let bottom = rect.y.saturating_add(rect.height as i32);
if matches!(self.mode, CrosshairMode::Vertical | CrosshairMode::Both) {
context.draw_line_stroke_aa(
Point::new(point.x, top),
Point::new(point.x, bottom),
color,
width,
);
}
if matches!(self.mode, CrosshairMode::Horizontal | CrosshairMode::Both) {
context.draw_line_stroke_aa(
Point::new(left, point.y),
Point::new(right, point.y),
color,
width,
);
}
if self.show_marker {
context.fill_circle(point, 3, color);
}
if self.show_axis_values {
if let Some((x, y)) = self.data {
let label = std::format!("{x:.2}, {y:.2}");
let font = crate::widget::metrics::effective_font(self.base.style());
context.draw_text(
Point::new(left + 2, top + 2),
&label,
font,
color,
HorizontalAlignment::Left,
);
}
}
}
}
impl crate::event::EventHandler for ChartCrosshair {
fn handle_event(&mut self, _event: &crate::event::Event) {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn show_and_hide_publish_the_data_coordinate() {
let mut crosshair = ChartCrosshair::new(Rect::new(0, 0, 200, 100));
assert!(!crosshair.is_visible());
let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let sink = std::sync::Arc::clone(&seen);
crosshair.crosshair_moved.connect(move |value| {
if let Ok(mut guard) = sink.lock() {
guard.push(*value);
}
});
crosshair.show_at_data(Point::new(20, 30), (1.5, 2.5));
assert!(crosshair.is_visible());
assert_eq!(crosshair.data_value(), Some((1.5, 2.5)));
crosshair.hide();
assert!(!crosshair.is_visible());
assert_eq!(
*seen.lock().expect("lock"),
vec![Some((1.5, 2.5)), None],
"show publishes the value, hide publishes None"
);
}
#[test]
fn mode_tokens_round_trip() {
for mode in [CrosshairMode::Vertical, CrosshairMode::Horizontal, CrosshairMode::Both] {
assert_eq!(CrosshairMode::from_name(mode.as_str()), Some(mode));
}
assert_eq!(CrosshairMode::from_name("diagonal"), None);
}
#[test]
fn drawing_a_shown_crosshair_reaches_the_backend() {
let mut crosshair = ChartCrosshair::new(Rect::new(0, 0, 200, 100));
crosshair.show_at_data(Point::new(40, 50), (1.0, 2.0));
let svg = crate::widget::svg::render_to_svg(&mut crosshair);
assert!(svg.contains("line"), "the reference lines were drawn");
}
}