Skip to main content

iced_aksel/
lib.rs

1//! A high-performance plotting library for Iced applications.
2//!
3//! `iced_aksel` provides interactive charts and plots for the Iced GUI framework,
4//! built on top of the `aksel` plotting core. It offers flexible axis configuration,
5//! multiple shape primitives, and robust interaction handling.
6//!
7//! # Quick Start
8//!
9//! To create a simple chart, you need to:
10//! 1. Define your [`State`] (which stores axes configuration).
11//! 2. Implement [`PlotData`] for your data type.
12//! 3. Instantiate the [`Chart`] widget in your view logic.
13//!
14//! ```rust,no_run
15//! use iced_aksel::{
16//!     Chart, State, Axis, Plot, PlotPoint, axis, scale::Linear,
17//!     plot::PlotData, shape::Ellipse, Measure
18//! };
19//! use iced::{Element, Theme};
20//!
21//! struct App {
22//!     chart_state: State<&'static str, f64>,
23//!     data: ScatterData,
24//! }
25//!
26//! #[derive(Debug, Clone)]
27//! enum Message {}
28//!
29//! impl App {
30//!     fn new() -> Self {
31//!         let mut chart_state = State::new();
32//!         // Register axes with unique IDs
33//!         chart_state.set_axis("x", Axis::new(Linear::new(0.0, 100.0), axis::Position::Bottom));
34//!         chart_state.set_axis("y", Axis::new(Linear::new(0.0, 100.0), axis::Position::Left));
35//!
36//!         Self {
37//!             chart_state,
38//!             data: ScatterData {
39//!                 points: vec![
40//!                     PlotPoint::new(10.0, 20.0),
41//!                     PlotPoint::new(50.0, 80.0),
42//!                 ],
43//!             },
44//!         }
45//!     }
46//!
47//!     fn view(&self) -> Element<Message> {
48//!         // Render the chart using the persistent state
49//!         Chart::new(&self.chart_state)
50//!             .plot_data(&self.data, "x", "y")
51//!             .into()
52//!     }
53//! }
54//!
55//! // Your data struct
56//! struct ScatterData {
57//!     points: Vec<PlotPoint<f64>>,
58//! }
59//!
60//! // Implement PlotData to define how your data is drawn
61//! impl PlotData<f64> for ScatterData {
62//!     fn draw(&self, plot: &mut Plot<f64>, theme: &Theme) {
63//!         for point in &self.points {
64//!             plot.add_shape(
65//!                 Ellipse::new(*point, Measure::Screen(5.0), Measure::Screen(5.0))
66//!                     .fill(theme.palette().primary)
67//!             );
68//!         }
69//!     }
70//! }
71//! ```
72//!
73//! # Core Concepts
74//!
75//! - **[`Chart`]**: The main widget that renders axes and data. It handles layout and user events.
76//! - **[`State`]**: A persistent struct that manages axis configuration. You should store this in your application's state.
77//! - **[`Axis`]**: Configures scales (Linear, Log), ticks, grid lines, and labels.
78//! - **[`PlotData`]**: A trait you implement for your own data types to define how they should be rendered.
79//! - **[`Shape`]**: Visual primitives (lines, circles, rectangles) used within `PlotData::draw`.
80
81use aksel::ScreenRect;
82use derive_more::{Display, Error};
83use iced_core::{
84    Clipboard, Color, Element, Event, Font, Layout, Length, Padding, Pixels, Point, Rectangle,
85    Shell, Size, Widget, keyboard,
86    layout::{self, Limits, Node},
87    mouse,
88    renderer::{Quad, Style},
89    widget::{Tree, tree},
90};
91use std::fmt::Debug;
92use std::hash::Hash;
93use std::ops::Deref;
94
95// Re-export aksel core types for convenience
96pub use aksel::{Float, Transform, scale, scale::Scale, transform, transform::PlotPoint};
97
98mod action;
99mod event;
100mod layer;
101mod measure;
102mod memory;
103mod render;
104mod state;
105
106pub mod axis;
107pub mod interaction;
108pub mod plot;
109pub mod radii;
110pub mod shape;
111pub mod stroke;
112pub mod style;
113
114pub use axis::Axis;
115pub use event::*;
116pub use interaction::Interaction;
117pub use layer::{Cached, LayerId};
118pub use measure::Measure;
119pub use plot::{Plot, PlotData};
120pub use radii::{Radii, Radius};
121pub use render::{Quality, Renderer};
122pub use shape::Shape;
123pub use state::State;
124pub use stroke::Stroke;
125pub use style::Catalog;
126
127use crate::interaction::{Area, InteractionQuery};
128use crate::memory::{CacheSignature, HoverIdentity};
129use crate::render::{LineArrows, LineExtensions, Primitive};
130use crate::stroke::ResolvedStroke;
131use action::Action;
132use axis::{MarkerContext, MarkerPosition, MarkerRequest, Orientation, Position};
133use layer::Layer;
134use memory::Memory;
135
136/// Default movement threshold (in pixels) to distinguish a click from a drag operation.
137const DEFAULT_DRAG_DEADBAND: f32 = 5.0;
138const INTERACTION_HIT_TOLERANCE: Pixels = Pixels(1.0);
139
140/// Errors that can occur during chart construction or rendering.
141#[derive(Debug, Clone, Error, Display)]
142pub enum Error<AxisId> {
143    /// Two axes with the same ID were assigned to a single layer.
144    #[display("Duplicate axis id's received for a layer: {id:?}")]
145    DuplicateAxis {
146        /// The id of the axis that was duplicated
147        id: AxisId,
148    },
149    /// Two axes have conflicting orientations (e.g., both are horizontal).
150    #[display(
151        "Conflicting axis orientations: {horizontal:?}({horizontal_orientation:?}) | {vertical:?}(vertical_orientation:?)"
152    )]
153    AxisConflict {
154        /// The ID of the horizontal axis
155        horizontal: AxisId,
156        /// The orientation of the horizontal axis
157        horizontal_orientation: Orientation,
158        /// The ID of the vertical axis
159        vertical: AxisId,
160        /// The orientation of the vertical axis
161        vertical_orientation: Orientation,
162    },
163    /// Referenced an axis ID that doesn't exist in the State.
164    #[display("Unknown axis id: '{id:?}'")]
165    UnknownAxis {
166        /// The ID of the unknown axis
167        id: AxisId,
168    },
169}
170
171// Internal type aliases for plot event handlers
172type ErrorHandler<AxisId, Message> = event::Handler<Message, (Error<AxisId>,)>;
173type MoveHandler<Message> = event::Handler<Message, (MoveEvent<Point>,)>;
174type EnterHandler<Message> = event::Handler<Message, (EnterEvent,)>;
175type ExitHandler<Message> = event::Handler<Message, (ExitEvent,)>;
176type DragHandler<Message> = event::Handler<Message, (DragEvent<Delta>,)>;
177type ScrollHandler<Message> = event::Handler<Message, (ScrollEvent<Point>,)>;
178type PressHandler<Message> = event::Handler<Message, (PressEvent<Point>,)>;
179type ReleaseHandler<Message> = event::Handler<Message, (ReleaseEvent<Point>,)>;
180
181// Internal type aliases for axis event handlers
182type AxisMoveHandler<AxisId, Message> = event::Handler<Message, (AxisId, MoveEvent<f32>)>;
183type AxisEnterHandler<AxisId, Message> = event::Handler<Message, (AxisId, EnterEvent)>;
184type AxisExitHandler<AxisId, Message> = event::Handler<Message, (AxisId, ExitEvent)>;
185type AxisDragHandler<AxisId, Message> = event::Handler<Message, (AxisId, DragEvent<f32>)>;
186type AxisScrollHandler<AxisId, Message> = event::Handler<Message, (AxisId, ScrollEvent<f32>)>;
187type AxisPressHandler<AxisId, Message> = event::Handler<Message, (AxisId, PressEvent<f32>)>;
188type AxisReleaseHandler<AxisId, Message> = event::Handler<Message, (AxisId, ReleaseEvent<f32>)>;
189
190/// The main charting widget that renders axes and plot data.
191///
192/// `Chart` manages the layout and rendering of axes, grid lines, and data layers.
193/// It supports rich interactions including click, drag, scroll, and hover events
194/// on both the plot area and individual axes.
195///
196/// # Example
197///
198/// ```rust,no_run
199/// use iced_aksel::{Chart, State, Axis, axis, scale::Linear, plot::PlotData};
200///
201/// # #[derive(Clone)]
202/// # enum Message { Scroll(iced::Point, iced::mouse::ScrollDelta) }
203/// # struct MyData;
204/// # impl PlotData<f64> for MyData {
205/// #     fn draw(&self, plot: &mut iced_aksel::Plot<f64>, theme: &iced::Theme) {}
206/// # }
207/// let mut state: State<&str, f64> = State::new();
208/// state.set_axis("x_axis", Axis::new(Linear::new(0.0, 100.0), axis::Position::Bottom));
209/// state.set_axis("y_axis", Axis::new(Linear::new(0.0, 100.0), axis::Position::Left));
210///
211/// let data = MyData;
212///
213/// let chart = Chart::new(&state)
214///     .plot_data(&data, "x_axis", "y_axis")
215///     .on_scroll(|pos, delta| Message::Scroll(pos, delta));
216/// ```
217pub struct Chart<
218    'a,
219    AxisId,
220    Domain,
221    Message,
222    Tag = (),
223    Theme = iced_core::Theme,
224    Renderer = iced_renderer::Renderer,
225> where
226    AxisId: Hash + Eq + Clone + Debug,
227    Domain: Float,
228    Theme: Catalog,
229    Renderer: crate::Renderer,
230    Message: Clone,
231    Tag: Hash + Eq + Clone,
232{
233    state: &'a State<AxisId, Domain, Theme>,
234    layers: Vec<Layer<'a, AxisId, Domain, Message, Tag, Renderer, Theme>>,
235    width: Length,
236    height: Length,
237    class: <Theme as Catalog>::Class<'a>,
238    errors: Vec<Error<AxisId>>,
239    drag_deadband: f32,
240    padding: Padding,
241    quality: Quality,
242    markers: Vec<MarkerRequest<'a, AxisId, Domain, Theme>>,
243
244    // Fonts
245    axis_font: Option<Font>,
246
247    // Interaction Handlers
248    on_error: Option<ErrorHandler<AxisId, Message>>,
249
250    // Cursor
251    default_cursor: mouse::Interaction,
252
253    // Plot Area Handlers
254    on_press: Option<PressHandler<Message>>,
255    on_release: Option<ReleaseHandler<Message>>,
256    on_drag: Option<DragHandler<Message>>,
257    on_enter: Option<EnterHandler<Message>>,
258    on_exit: Option<ExitHandler<Message>>,
259    on_move: Option<MoveHandler<Message>>,
260    on_scroll: Option<ScrollHandler<Message>>,
261
262    // Axis Handlers
263    on_axis_press: Option<AxisPressHandler<AxisId, Message>>,
264    on_axis_release: Option<AxisReleaseHandler<AxisId, Message>>,
265    on_axis_drag: Option<AxisDragHandler<AxisId, Message>>,
266    on_axis_move: Option<AxisMoveHandler<AxisId, Message>>,
267    on_axis_enter: Option<AxisEnterHandler<AxisId, Message>>,
268    on_axis_exit: Option<AxisExitHandler<AxisId, Message>>,
269    on_axis_scroll: Option<AxisScrollHandler<AxisId, Message>>,
270
271    debug: bool,
272}
273
274impl<'a, AxisId, Domain, Message: std::clone::Clone, Tag, Theme, Renderer>
275    Chart<'a, AxisId, Domain, Message, Tag, Theme, Renderer>
276where
277    Domain: Float,
278    AxisId: Hash + Eq + Clone + Debug,
279    Tag: Hash + Eq + Clone + Debug,
280    Theme: Catalog,
281    Renderer: crate::Renderer,
282{
283    /// Creates a new chart from the given state.
284    ///
285    /// The `State` contains the configuration of all axes. It is separated from the
286    /// `Chart` widget to allow persistence across frames and ease of manipulation
287    /// (e.g., zooming/panning) from your update logic.
288    pub fn new(state: &'a State<AxisId, Domain, Theme>) -> Self {
289        Self {
290            state,
291            layers: vec![],
292            width: Length::Fill,
293            height: Length::Fill,
294            class: <Theme as Catalog>::default(),
295            errors: vec![],
296            drag_deadband: DEFAULT_DRAG_DEADBAND,
297            padding: Padding::new(0.),
298            quality: Quality::Medium,
299            markers: Vec::with_capacity(state.axes().len()),
300
301            // Handlers and fonts default to None
302            axis_font: None,
303            on_error: None,
304
305            default_cursor: mouse::Interaction::Idle,
306
307            on_drag: None,
308            on_enter: None,
309            on_exit: None,
310            on_move: None,
311            on_scroll: None,
312            on_press: None,
313            on_release: None,
314
315            on_axis_press: None,
316            on_axis_release: None,
317            on_axis_drag: None,
318            on_axis_move: None,
319            on_axis_enter: None,
320            on_axis_exit: None,
321            on_axis_scroll: None,
322
323            debug: false,
324        }
325    }
326
327    /// Sets the style of the chart.
328    pub fn style(mut self, style: <Theme as Catalog>::Class<'a>) -> Self {
329        self.class = style;
330        self
331    }
332
333    /// Enables or disables the debug overlay.
334    ///
335    /// When enabled, an overlay will display performance metrics such as
336    /// vertex and index counts in the corner of the chart.
337    pub const fn debug(mut self, debug: bool) -> Self {
338        self.debug = debug;
339        self
340    }
341
342    /// Sets the global rendering quality multiplier.
343    ///
344    /// Controls the Level of Detail (LOD) for curves and text.
345    /// * `1.0`: Standard quality (Default).
346    /// * `< 1.0`: Lower quality, higher performance.
347    /// * `> 1.0`: Higher quality, smoother curves.
348    pub const fn quality(mut self, quality: Quality) -> Self {
349        self.quality = quality;
350        self
351    }
352
353    /// Sets the font used to render the [`Axis`] labels and [`axis::Marker`]
354    pub const fn axes_font(mut self, font: Font) -> Self {
355        self.axis_font = Some(font);
356        self
357    }
358
359    /// Adds a data layer to the chart.
360    ///
361    /// The data will be plotted using the coordinate system defined by the two specified axes.
362    /// Multiple layers can be added to a single chart, potentially using different axes.
363    ///
364    /// ***OBS***: It's important to note that the axis ID's **must** be present in [`State`]
365    pub fn plot_data<T: plot::PlotData<Domain, Message, Tag, Renderer, Theme>>(
366        mut self,
367        items: &'a T,
368        x_axis_id: AxisId,
369        y_axis_id: AxisId,
370    ) -> Self {
371        let layer = Layer::new(items, x_axis_id, y_axis_id);
372        if verify_layer(&layer, self.state, &mut self.errors) {
373            self.layers.push(layer);
374        }
375
376        self
377    }
378
379    /// Sets the width of the chart.
380    pub const fn width(mut self, width: Length) -> Self {
381        self.width = width;
382        self
383    }
384
385    /// Sets the height of the chart.
386    pub const fn height(mut self, height: Length) -> Self {
387        self.height = height;
388        self
389    }
390
391    /// Sets the padding around the chart.
392    ///
393    /// This adds space between the widget bounds and the chart contents (axes/plot).
394    pub const fn padding(mut self, padding: Padding) -> Self {
395        self.padding = padding;
396        self
397    }
398
399    /// Sets the minimum drag distance in pixels before drag events are triggered.
400    ///
401    /// Default is 10 pixels. This helps prevent accidental drags when the user intended to click.
402    pub const fn drag_deadband(mut self, distance: f32) -> Self {
403        self.drag_deadband = distance;
404        self
405    }
406
407    /// Sets the default mouse cursor to display when hovering over the empty plot background.
408    ///
409    /// If an interactive shape overrides the cursor, this background cursor will be temporarily hidden.
410    pub const fn default_cursor(mut self, cursor: mouse::Interaction) -> Self {
411        self.default_cursor = cursor;
412        self
413    }
414
415    /// Sets a marker to be drawn on the given axis, at the given position, if the position is Some
416    pub fn marker_maybe<F>(
417        mut self,
418        axis_id: &'a AxisId,
419        position: Option<MarkerPosition<Domain>>,
420        renderer: F,
421    ) -> Self
422    where
423        F: for<'ctx> Fn(MarkerContext<'ctx, Domain, Theme>) -> Option<axis::Marker> + 'static,
424    {
425        if let Some(position) = position {
426            self.markers.push(MarkerRequest {
427                axis_id,
428                position,
429                renderer: Box::new(renderer),
430            });
431        }
432        self
433    }
434
435    /// Sets a marker to be drawn on the given axis, at the given position
436    pub fn marker<F>(
437        mut self,
438        axis_id: &'a AxisId,
439        position: MarkerPosition<Domain>,
440        renderer: F,
441    ) -> Self
442    where
443        F: for<'ctx> Fn(MarkerContext<'ctx, Domain, Theme>) -> Option<axis::Marker> + 'static,
444    {
445        self.markers.push(MarkerRequest {
446            axis_id,
447            position,
448            renderer: Box::new(renderer),
449        });
450        self
451    }
452
453    event::impl_handlers!(
454        /// Sets the event handler for chart configuration errors.
455        ///
456        /// Errors can occur when axes referenced in `plot_data` are missing from the `State`
457        /// or have conflicting orientations.
458        error: (Error<AxisId>,);
459
460        /// Sets the event handler for mouse presses on the main plot area.
461        press: (PressEvent<Point>,);
462
463        /// Sets the event handler for mouse releases on the main plot area.
464        release: (ReleaseEvent<Point>,);
465
466        /// Sets the event handler for drag events on the main plot area.
467        drag: (DragEvent<Delta>,);
468
469        /// Sets the event handler for when the mouse hovers the main plot area.
470        move: (MoveEvent<Point>,);
471
472        /// Sets the event handler for when the mouse enters the plot
473        enter: (EnterEvent,);
474
475        /// Sets the event handler for when the mouse exits the plot
476        exit: (ExitEvent,);
477
478        /// Sets a callback for scroll events (mouse wheel) on the main plot area.
479        scroll: (ScrollEvent<Point>,);
480
481
482        /// Sets the event handler for mouse presses on an axis.
483        axis_press: (AxisId, PressEvent<f32>);
484
485        /// Sets the event handler for mouse releases on an axis.
486        axis_release: (AxisId, ReleaseEvent<f32>);
487
488        /// Sets the event handler for dragging on an axis.
489        axis_drag: (AxisId, DragEvent<f32>);
490
491        /// Sets the event handler for hovering on an axis.
492        axis_move: (AxisId, MoveEvent<f32>);
493
494        /// Sets the event handler for hovering on an axis.
495        axis_enter: (AxisId, EnterEvent);
496
497        /// Sets the event handler for hovering on an axis.
498        axis_exit: (AxisId, ExitEvent);
499
500        /// Sets the event handler for scrolling on an axis
501        axis_scroll: (AxisId, ScrollEvent<f32>);
502    );
503
504    /// Internal handler for mouse press events.
505    /// Determines if the user mouse-pressed on the plot or an axis and updates the internal state.
506    fn handle_mouse_press(
507        &self,
508        memory: &mut Memory<AxisId, Message, Tag, Renderer>,
509        layout: Layout,
510        shell: &mut Shell<'_, Message>,
511        click: mouse::Click,
512        button: mouse::Button,
513    ) {
514        // If we press during any other action than idle, we must return
515        if Action::Idle != memory.action {
516            return;
517        }
518
519        let plot_bounds = self.get_plot_layout(layout).bounds();
520        let mouse_pos = click.position();
521
522        // Check if press is on the plot area
523        if plot_bounds.contains(mouse_pos) {
524            shell.capture_event();
525
526            let interactions = memory.interaction_cache.borrow();
527
528            // Build the query with a 5px hover/click tolerance
529            let query = InteractionQuery::Point {
530                position: mouse_pos,
531                tolerance: INTERACTION_HIT_TOLERANCE,
532            };
533
534            // Query for prioritized targets with `on_press` handlers
535            let target_id = interactions
536                .query_prioritized(&query, |interaction| interaction.on_press.is_some())
537                // Publish the target's event if any, and save the id to put into memory later
538                .map(|(id, interaction)| {
539                    // SAFETY: We just checked if the on_press handler exists in the prioritized query
540                    // above
541                    let handler = interaction.on_press.as_ref().unwrap();
542
543                    let normalized = Point::new(
544                        (mouse_pos.x - plot_bounds.x) / plot_bounds.width,
545                        1.0 - ((mouse_pos.y - plot_bounds.y) / plot_bounds.height),
546                    );
547
548                    let event = PressEvent::new(
549                        normalized,
550                        button,
551                        click.kind(),
552                        memory.keyboard_modifiers,
553                    );
554
555                    if let Some(message) = handler.run((id.clone(), event)) {
556                        shell.publish(message);
557                    }
558
559                    id
560                });
561
562            let handled = target_id.is_some();
563
564            memory.action = Action::DraggingPlot {
565                interaction_id: target_id,
566                origin: mouse_pos,
567                last_position: mouse_pos,
568                total_delta: 0.0,
569                button,
570                click_kind: click.kind(),
571            };
572
573            // Make sure we don't test plot, if a shape was pressed already
574            if handled {
575                return;
576            }
577
578            if let Some(handler) = &self.on_press {
579                let normalized = Point::new(
580                    (mouse_pos.x - plot_bounds.x) / plot_bounds.width,
581                    1.0 - ((mouse_pos.y - plot_bounds.y) / plot_bounds.height),
582                );
583                let event =
584                    PressEvent::new(normalized, button, click.kind(), memory.keyboard_modifiers);
585                if let Some(message) = handler.run((event,)) {
586                    shell.publish(message);
587                }
588            }
589
590            return;
591        }
592
593        // Check if press is on any axis
594        for (i, (id, axis)) in self.state.axes().iter().enumerate() {
595            let axis_bounds = layout.children().nth(i).unwrap().bounds();
596
597            if !axis_bounds.contains(mouse_pos) {
598                continue;
599            }
600
601            let origin = match axis.orientation() {
602                Orientation::Horizontal => mouse_pos.x,
603                Orientation::Vertical => mouse_pos.y,
604            };
605
606            shell.capture_event();
607
608            memory.action = Action::DraggingAxis {
609                id: id.clone(),
610                origin,
611                last_position: origin,
612                total_delta: 0.0,
613                button,
614                click_kind: click.kind(),
615            };
616
617            // Handle double-click on axis
618            if let Some(handler) = self.on_axis_press.as_ref()
619                && let Some(message) = handler.run((
620                    id.clone(),
621                    PressEvent::new(
622                        axis.screen_to_normalized(origin, &axis_bounds),
623                        button,
624                        click.kind(),
625                        memory.keyboard_modifiers,
626                    ),
627                ))
628            {
629                shell.publish(message);
630            }
631
632            // We can only interact with one axis at a time
633            return;
634        }
635    }
636
637    /// Internal handler for mouse release events.
638    /// Triggers click events if the drag distance was within the deadband.
639    fn handle_mouse_release(
640        &self,
641        memory: &mut Memory<AxisId, Message, Tag, Renderer>,
642        layout: Layout,
643        shell: &mut Shell<'_, Message>,
644        previous_click_kind: Option<mouse::click::Kind>,
645        button: mouse::Button,
646    ) {
647        let Memory { action, .. } = memory;
648
649        // If total drag exceeded deadband, it was a drag, not a click.
650        let was_dragging = action
651            .total_drag_delta()
652            .is_some_and(|delta| delta > self.drag_deadband);
653
654        match action {
655            Action::Idle => (), // Do nothing
656            Action::DraggingPlot {
657                origin,
658                interaction_id,
659                ..
660            } => {
661                let plot_bounds = self.get_plot_layout(layout).bounds();
662                let interactions = memory.interaction_cache.borrow();
663
664                let normalized = Point::new(
665                    (origin.x - plot_bounds.x) / plot_bounds.width,
666                    1.0 - ((origin.y - plot_bounds.y) / plot_bounds.height),
667                );
668                let event = ReleaseEvent::new(
669                    normalized,
670                    button,
671                    previous_click_kind,
672                    memory.keyboard_modifiers,
673                    was_dragging,
674                );
675
676                // If interaction started on another interaction, we should prefer using that
677                if let Some(existing_id) = interaction_id
678                    && let Some(interaction) = interactions.get(existing_id)
679                    && let Some(handler) = interaction.on_release.as_ref()
680                {
681                    // Publish message if any and return - Also return otherwise, so we don't
682                    // process any other events
683                    if let Some(message) = handler.run((existing_id.clone(), event)) {
684                        shell.publish(message)
685                    }
686                    return;
687                }
688
689                // If we have no "current" interaction, we look for one before handling chart
690                let query = InteractionQuery::Point {
691                    position: *origin,
692                    tolerance: INTERACTION_HIT_TOLERANCE,
693                };
694                let target_id = interactions
695                    .query_prioritized(&query, |interaction| interaction.on_release.is_some())
696                    .map(|(id, interaction)| {
697                        // SAFETY: We just checked if the on_press handler exists in the prioritized query
698                        // above
699                        let handler = interaction.on_release.as_ref().unwrap();
700
701                        if let Some(message) = handler.run((id.clone(), event)) {
702                            shell.publish(message);
703                        }
704
705                        id
706                    });
707
708                if target_id.is_some() {
709                    // Don't handle chart if we already hit an interaction
710                    return;
711                }
712
713                if let Some(handler) = &self.on_release
714                    && let Some(message) = handler.run((event,))
715                {
716                    shell.publish(message);
717                }
718            }
719            Action::DraggingAxis { id, origin, .. } => {
720                if let Some((i, id, axis)) = self.state.axes().get_full(id) {
721                    let axis_bounds = layout.children().nth(i).unwrap().bounds();
722                    let normalized = axis.screen_to_normalized(*origin, &axis_bounds);
723                    if let Some(handler) = &self.on_axis_release
724                        && let Some(message) = handler.run((
725                            id.clone(),
726                            ReleaseEvent::new(
727                                normalized,
728                                button,
729                                previous_click_kind,
730                                memory.keyboard_modifiers,
731                                was_dragging,
732                            ),
733                        ))
734                    {
735                        shell.publish(message);
736                    }
737                }
738            }
739        }
740    }
741
742    /// Internal handler for mouse movement.
743    /// Manages hover states and processes drag deltas.
744    ///
745    /// Return the last hover identity if hover identity has changed
746    fn handle_mouse_moved(
747        &self,
748        memory: &mut Memory<AxisId, Message, Tag, Renderer>,
749        layout: Layout,
750        shell: &mut Shell<'_, Message>,
751        mouse_pos: Point,
752    ) -> Option<HoverIdentity<AxisId, Tag>> {
753        let Memory { action, .. } = memory;
754        let plot_bounds = self.get_plot_layout(layout).bounds();
755
756        // Mouse is over the plot area
757        if plot_bounds.contains(mouse_pos) {
758            match action {
759                Action::DraggingAxis { .. } => (), // Ignore if dragging axis
760                Action::Idle => {
761                    if let Some(handler) = &self.on_move
762                        && let Some(message) = handler.run((MoveEvent::new(
763                            Point::new(
764                                (mouse_pos.x - plot_bounds.x) / plot_bounds.width,
765                                1.0 - ((mouse_pos.y - plot_bounds.y) / plot_bounds.height),
766                            ),
767                            memory.keyboard_modifiers,
768                        ),))
769                    {
770                        shell.publish(message);
771                    }
772
773                    // Check the Interaction Registry for hovers!
774                    let interactions = memory.interaction_cache.borrow();
775                    let query = InteractionQuery::Point {
776                        position: mouse_pos,
777                        tolerance: INTERACTION_HIT_TOLERANCE,
778                    };
779
780                    // Check for prioritized `on_enter` interaction
781                    let prioritized_id = interactions
782                        .query_prioritized(&query, |interaction| interaction.on_enter.is_some())
783                        .map(|(id, _)| HoverIdentity::Interaction(id));
784
785                    let identity = prioritized_id.unwrap_or(HoverIdentity::Plot);
786
787                    if memory.last_hovered_identity != identity {
788                        let last = std::mem::replace(&mut memory.last_hovered_identity, identity);
789                        return Some(last);
790                    }
791
792                    return None;
793                }
794                Action::DraggingPlot {
795                    last_position,
796                    total_delta,
797                    interaction_id,
798                    button,
799                    click_kind,
800                    ..
801                } => {
802                    let delta_x = mouse_pos.x - last_position.x;
803                    let delta_y = mouse_pos.y - last_position.y;
804                    *total_delta += delta_x.hypot(delta_y);
805                    *last_position = mouse_pos;
806
807                    if *total_delta < self.drag_deadband {
808                        return None;
809                    };
810
811                    // Interaction present - Use that instead
812                    if let Some(id) = interaction_id {
813                        shell.capture_event();
814
815                        let interactions = memory.interaction_cache.borrow();
816                        let handler = interactions.get(id)?.on_drag.as_ref()?;
817
818                        // For interaction: shape/interaction moves with cursor
819                        // x: positive right, y: negative down (chart coords go up)
820                        let normalized_delta = Delta {
821                            x: delta_x / plot_bounds.width,
822                            y: -delta_y / plot_bounds.height,
823                        };
824
825                        let event = DragEvent::new(
826                            normalized_delta,
827                            *button,
828                            *click_kind,
829                            memory.keyboard_modifiers,
830                        );
831
832                        if let Some(message) = handler.run((id.clone(), event)) {
833                            shell.publish(message);
834                            // Drag events can never propagate, so we return here
835                            return None;
836                        };
837                    }
838
839                    if let Some(handler) = &self.on_drag {
840                        shell.capture_event();
841
842                        // For chart: dragging right pans chart right (data moves left)
843                        // x: negative right, y: positive down
844                        let normalized_delta = Delta {
845                            x: -delta_x / plot_bounds.width,
846                            y: delta_y / plot_bounds.height,
847                        };
848
849                        let event = DragEvent::new(
850                            normalized_delta,
851                            *button,
852                            *click_kind,
853                            memory.keyboard_modifiers,
854                        );
855
856                        if let Some(message) = handler.run((event,)) {
857                            shell.publish(message);
858                        }
859                    }
860
861                    return None;
862                }
863            }
864        }
865
866        // Handle active axis drag
867        if let Action::DraggingAxis {
868            id: dragging_id,
869            last_position,
870            total_delta,
871            button,
872            click_kind,
873            ..
874        } = action
875        {
876            shell.capture_event();
877            if let Some((i, id, axis)) = self.state.axes().get_full(dragging_id) {
878                let axis_bounds = layout.children().nth(i).unwrap().bounds();
879                let screen_value = match axis.orientation() {
880                    axis::Orientation::Horizontal => mouse_pos.x,
881                    axis::Orientation::Vertical => mouse_pos.y,
882                };
883
884                let delta = screen_value - *last_position;
885                *total_delta += delta.abs();
886                *last_position = screen_value;
887
888                if *total_delta > self.drag_deadband
889                    && let Some(handler) = &self.on_axis_drag
890                {
891                    let normalized_delta = axis.translate_drag_delta(delta, &axis_bounds);
892                    let event = DragEvent::new(
893                        normalized_delta,
894                        *button,
895                        *click_kind,
896                        memory.keyboard_modifiers,
897                    );
898                    if let Some(message) = handler.run((id.clone(), event)) {
899                        shell.publish(message);
900                    }
901                }
902            }
903        }
904        // Handle axis hover
905        else if matches!(action, Action::Idle) {
906            for (i, (id, axis)) in self.state.axes().iter().enumerate() {
907                let axis_bounds = layout.children().nth(i).unwrap().bounds();
908
909                if !axis_bounds.contains(mouse_pos) {
910                    memory.last_hovered_identity = HoverIdentity::Axis(id.clone());
911                    continue;
912                }
913
914                if let Some(handler) = &self.on_axis_move {
915                    let screen_value = match axis.orientation() {
916                        axis::Orientation::Horizontal => mouse_pos.x,
917                        axis::Orientation::Vertical => mouse_pos.y,
918                    };
919                    let normalized = axis.screen_to_normalized(screen_value, &axis_bounds);
920                    if let Some(message) = handler.run((
921                        id.clone(),
922                        MoveEvent::new(normalized, memory.keyboard_modifiers),
923                    )) {
924                        shell.publish(message);
925                    }
926                }
927
928                break;
929            }
930        }
931
932        None
933    }
934
935    #[inline(always)]
936    fn get_plot_layout<'b>(&self, layout: Layout<'b>) -> Layout<'b> {
937        // The plot area is always the last child in the layout list
938        layout.children().last().unwrap()
939    }
940
941    /// Iterates over axes to find the inner-most spines and draws connecting squares at the corners.
942    fn draw_spine_corners(
943        &self,
944        layout: Layout<'_>,
945        style: &style::Style,
946        plot: Rectangle,
947        renderer: &mut Renderer,
948    ) {
949        // Track the "inner-most" spine properties for each side
950        let mut left: Option<(f32, Color)> = None;
951        let mut right: Option<(f32, Color)> = None;
952        let mut top: Option<(f32, Color)> = None;
953        let mut bottom: Option<(f32, Color)> = None;
954
955        // Track the edge coordinates to determine closeness to the plot
956        let mut max_left_edge = f32::MIN;
957        let mut min_right_edge = f32::MAX;
958        let mut max_top_edge = f32::MIN;
959        let mut min_bottom_edge = f32::MAX;
960
961        // 1. Find the winners
962        for (i, (_, axis)) in self.state.axes().iter().enumerate() {
963            if !axis.is_visible() {
964                continue;
965            }
966
967            if style.axis.spine.width.0 <= 0.0 {
968                continue;
969            }
970
971            let style = axis.create_style(style).spine;
972            let bounds = layout.children().nth(i).unwrap().bounds();
973            let data = (style.width.0, style.color);
974
975            match axis.position() {
976                Position::Left => {
977                    let edge = bounds.x + bounds.width;
978                    if edge >= max_left_edge {
979                        max_left_edge = edge;
980                        left = Some(data);
981                    }
982                }
983                Position::Right => {
984                    let edge = bounds.x;
985                    if edge <= min_right_edge {
986                        min_right_edge = edge;
987                        right = Some(data);
988                    }
989                }
990                Position::Top => {
991                    let edge = bounds.y + bounds.height;
992                    if edge >= max_top_edge {
993                        max_top_edge = edge;
994                        top = Some(data);
995                    }
996                }
997                Position::Bottom => {
998                    let edge = bounds.y;
999                    if edge <= min_bottom_edge {
1000                        min_bottom_edge = edge;
1001                        bottom = Some(data);
1002                    }
1003                }
1004            }
1005        }
1006
1007        // 2. Render the corners
1008
1009        // Bottom-Left
1010        if let (Some((lw, lc)), Some((bw, _))) = (left, bottom) {
1011            let top_left = Point::new(plot.x - lw, plot.y + plot.height);
1012            let size = Size::new(lw, bw);
1013            renderer.fill_quad(
1014                Quad {
1015                    bounds: Rectangle::new(top_left, size),
1016                    snap: true,
1017                    ..Default::default()
1018                },
1019                lc,
1020            );
1021        }
1022
1023        // Top-Left
1024        if let (Some((lw, lc)), Some((tw, _))) = (left, top) {
1025            let top_left = Point::new(plot.x - lw, plot.y - tw);
1026            let size = Size::new(lw, tw);
1027            renderer.fill_quad(
1028                Quad {
1029                    bounds: Rectangle::new(top_left, size),
1030                    snap: true,
1031                    ..Default::default()
1032                },
1033                lc,
1034            );
1035        }
1036
1037        // Bottom-Right
1038        if let (Some((rw, rc)), Some((bw, _))) = (right, bottom) {
1039            let top_left = Point::new(plot.x + plot.width, plot.y + plot.height);
1040            let size = Size::new(rw, bw);
1041            renderer.fill_quad(
1042                Quad {
1043                    bounds: Rectangle::new(top_left, size),
1044                    snap: true,
1045                    ..Default::default()
1046                },
1047                rc,
1048            );
1049        }
1050
1051        // Top-Right
1052        if let (Some((rw, rc)), Some((tw, _))) = (right, top) {
1053            let top_left = Point::new(plot.x + plot.width, plot.y - tw);
1054            let size = Size::new(rw, tw);
1055            renderer.fill_quad(
1056                Quad {
1057                    bounds: Rectangle::new(top_left, size),
1058                    snap: true,
1059                    ..Default::default()
1060                },
1061                rc,
1062            );
1063        }
1064    }
1065}
1066
1067impl<AxisId, Domain, Message, Theme, Tag, Renderer> Widget<Message, Theme, Renderer>
1068    for Chart<'_, AxisId, Domain, Message, Tag, Theme, Renderer>
1069where
1070    AxisId: Hash + Eq + Debug + Clone + 'static,
1071    Domain: Float + 'static,
1072    Tag: Hash + Eq + Clone + Debug + 'static,
1073    Renderer: crate::Renderer + iced_core::text::Renderer<Font = iced_core::Font> + 'static,
1074    Theme: Catalog,
1075    Message: Clone + 'static,
1076{
1077    fn tag(&self) -> tree::Tag {
1078        tree::Tag::of::<Memory<AxisId, Message, Tag, Renderer>>()
1079    }
1080
1081    fn state(&self) -> tree::State {
1082        tree::State::new(Memory::<AxisId, Message, Tag, Renderer>::new())
1083    }
1084
1085    fn children(&self) -> Vec<Tree> {
1086        // One child per Axis + one for the content (plot area).
1087        let mut children: Vec<Tree> = self.state.axes().iter().map(|_| Tree::empty()).collect();
1088        children.push(Tree::empty()); // content
1089        children
1090    }
1091
1092    fn diff(&self, _tree: &mut Tree) {}
1093
1094    fn size(&self) -> Size<Length> {
1095        Size::new(self.width, self.height)
1096    }
1097
1098    fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
1099        let memory: &mut Memory<AxisId, Message, Tag, Renderer> = tree.state.downcast_mut();
1100        memory.make_sure_cache_is_initialized(renderer, self.quality);
1101
1102        let bounds = limits.resolve(self.width, self.height, Size::ZERO);
1103
1104        let axis_count = self.state.axes().len();
1105
1106        // Pass 1: Measure total thickness required for axes on each side
1107        let mut top_total = self.padding.top;
1108        let mut bottom_total = self.padding.bottom;
1109        let mut left_total = self.padding.left;
1110        let mut right_total = self.padding.right;
1111
1112        for (_, axis) in self.state.axes() {
1113            let thickness = axis.thickness().0;
1114            match axis.position() {
1115                Position::Top => top_total += thickness,
1116                Position::Bottom => bottom_total += thickness,
1117                Position::Left => left_total += thickness,
1118                Position::Right => right_total += thickness,
1119            }
1120        }
1121
1122        // Pass 2: Calculate the remaining area for the actual chart
1123        let chart_height = (bounds.height - top_total - bottom_total).max(0.0);
1124        let chart_width = (bounds.width - left_total - right_total).max(0.0);
1125
1126        let chart_origin = Point::new(left_total, top_total);
1127        let chart_size = Size::new(chart_width, chart_height);
1128
1129        // Pass 3: Generate layout nodes for everything
1130        let mut children_nodes = Vec::with_capacity(axis_count + 1);
1131
1132        let mut top_y = self.padding.top;
1133        let mut bot_y = top_total + chart_height;
1134        let mut left_x = self.padding.left;
1135        let mut right_x = left_total + chart_width;
1136
1137        for (_, axis) in self.state.axes() {
1138            let thickness = axis.thickness().0;
1139            let node = match axis.position() {
1140                Position::Top => {
1141                    let n = layout_horizontal_axis(chart_width, axis, left_total, top_y, thickness);
1142                    top_y += thickness;
1143                    n
1144                }
1145                Position::Bottom => {
1146                    let n = layout_horizontal_axis(chart_width, axis, left_total, bot_y, thickness);
1147                    bot_y += thickness;
1148                    n
1149                }
1150                Position::Left => {
1151                    let n = layout_vertical_axis(chart_height, axis, left_x, top_total, thickness);
1152                    left_x += thickness;
1153                    n
1154                }
1155                Position::Right => {
1156                    let n = layout_vertical_axis(chart_height, axis, right_x, top_total, thickness);
1157                    right_x += thickness;
1158                    n
1159                }
1160            };
1161            children_nodes.push(node);
1162        }
1163
1164        // Add the chart content node (center plot)
1165        let chart_node = Node::new(chart_size).move_to(chart_origin);
1166        children_nodes.push(chart_node);
1167
1168        Node::with_children(bounds, children_nodes)
1169    }
1170
1171    fn update(
1172        &mut self,
1173        tree: &mut Tree,
1174        event: &Event,
1175        layout: layout::Layout<'_>,
1176        cursor: mouse::Cursor,
1177        _renderer: &Renderer,
1178        _clipboard: &mut dyn Clipboard,
1179        shell: &mut Shell<'_, Message>,
1180        _viewport: &Rectangle,
1181    ) {
1182        if !self.errors.is_empty()
1183            && let Some(handler) = &self.on_error
1184        {
1185            for error in self.errors.drain(..) {
1186                if let Some(message) = handler.run((error,)) {
1187                    shell.publish(message);
1188                }
1189            }
1190            return;
1191        }
1192
1193        let signature = CacheSignature::new(self.state, &layout, &self.layers);
1194        let memory: &mut Memory<AxisId, Message, Tag, Renderer> = tree.state.downcast_mut();
1195        memory.update(signature);
1196        memory.update_partitions(self.get_plot_layout(layout).bounds());
1197
1198        // Only handle events if the cursor is near the chart
1199        let bounds = layout.bounds();
1200        let Some(mouse_pos) = cursor.position_over(bounds) else {
1201            return;
1202        };
1203
1204        // Handle input events
1205        match event {
1206            Event::Mouse(mouse::Event::ButtonPressed(button)) => {
1207                let new_click = memory.update_click(mouse_pos, *button);
1208                self.handle_mouse_press(memory, layout, shell, new_click, *button);
1209            }
1210            Event::Mouse(mouse::Event::ButtonReleased(button)) => {
1211                let previous_click_kind = memory.previous_click.take().map(|c| c.kind());
1212                self.handle_mouse_release(memory, layout, shell, previous_click_kind, *button);
1213                memory.action = Action::Idle;
1214            }
1215            Event::Mouse(mouse::Event::CursorMoved { position }) => {
1216                let changed = self.handle_mouse_moved(memory, layout, shell, *position);
1217
1218                let Some(last) = changed else {
1219                    shell.request_redraw();
1220                    return;
1221                };
1222
1223                // match the last event
1224                let exit_message = match last {
1225                    HoverIdentity::Plot => self.on_exit.as_ref().and_then(|handler| {
1226                        handler.run((ExitEvent::new(memory.keyboard_modifiers),))
1227                    }),
1228                    HoverIdentity::Axis(id) => self.on_axis_exit.as_ref().and_then(|handler| {
1229                        handler.run((id.clone(), ExitEvent::new(memory.keyboard_modifiers)))
1230                    }),
1231                    HoverIdentity::Interaction(id) => memory
1232                        .interaction_cache
1233                        .borrow()
1234                        .get(&id)
1235                        .and_then(|interaction| {
1236                            interaction.on_exit.as_ref().map(|handler| {
1237                                handler.run((id.clone(), ExitEvent::new(memory.keyboard_modifiers)))
1238                            })
1239                        })
1240                        .flatten(),
1241                    HoverIdentity::OutsideBounds => None, // Do nothing
1242                };
1243
1244                // Match the new event:
1245                let enter_message = match &memory.last_hovered_identity {
1246                    HoverIdentity::Plot => self.on_enter.as_ref().and_then(|handler| {
1247                        handler.run((EnterEvent::new(memory.keyboard_modifiers),))
1248                    }),
1249                    HoverIdentity::Axis(id) => self.on_axis_enter.as_ref().and_then(|handler| {
1250                        handler.run((id.clone(), EnterEvent::new(memory.keyboard_modifiers)))
1251                    }),
1252                    HoverIdentity::Interaction(id) => memory
1253                        .interaction_cache
1254                        .borrow()
1255                        .get(id)
1256                        .and_then(|interaction| {
1257                            interaction.on_enter.as_ref().map(|handler| {
1258                                handler
1259                                    .run((id.clone(), EnterEvent::new(memory.keyboard_modifiers)))
1260                            })
1261                        })
1262                        .flatten(),
1263                    HoverIdentity::OutsideBounds => None, // Do nothing
1264                };
1265
1266                if let Some(message) = exit_message {
1267                    shell.publish(message);
1268                }
1269
1270                if let Some(message) = enter_message {
1271                    shell.publish(message);
1272                }
1273            }
1274            Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
1275                if let Some(cursor_pos) = cursor.position() {
1276                    let plot_bounds = self.get_plot_layout(layout).bounds();
1277
1278                    // Check if scrolling over the plot area
1279                    if cursor.position_over(plot_bounds).is_some() {
1280                        if let Some(handler) = &self.on_scroll {
1281                            shell.capture_event();
1282
1283                            // Normalize cursor position (0.0-1.0)
1284                            let normalized = Point::new(
1285                                (cursor_pos.x - plot_bounds.x) / plot_bounds.width,
1286                                1.0 - ((cursor_pos.y - plot_bounds.y) / plot_bounds.height),
1287                            );
1288
1289                            let event =
1290                                ScrollEvent::new(normalized, *delta, memory.keyboard_modifiers);
1291
1292                            if let Some(message) = handler.run((event,)) {
1293                                shell.publish(message);
1294                            }
1295                        }
1296                    } else {
1297                        // Check if scrolling over an axis
1298                        for (i, (id, axis)) in self.state.axes().iter().enumerate() {
1299                            let axis_bounds = layout.children().nth(i).unwrap().bounds();
1300
1301                            if cursor.position_over(axis_bounds).is_some() {
1302                                if let Some(handler) = &self.on_axis_scroll {
1303                                    let screen_value = match axis.orientation() {
1304                                        Orientation::Horizontal => cursor_pos.x,
1305                                        Orientation::Vertical => cursor_pos.y,
1306                                    };
1307
1308                                    let normalized =
1309                                        axis.screen_to_normalized(screen_value, &axis_bounds);
1310
1311                                    shell.capture_event();
1312
1313                                    let event = ScrollEvent::new(
1314                                        normalized,
1315                                        *delta,
1316                                        memory.keyboard_modifiers,
1317                                    );
1318
1319                                    if let Some(message) = handler.run((id.clone(), event)) {
1320                                        shell.publish(message);
1321                                    }
1322                                }
1323                                break;
1324                            }
1325                        }
1326                    }
1327                }
1328            }
1329            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
1330                memory.update_modifiers(*modifiers)
1331            }
1332            // TODO: Add multi-touch support for zooming
1333            _ => {}
1334        }
1335
1336        shell.request_redraw();
1337    }
1338
1339    fn draw(
1340        &self,
1341        tree: &Tree,
1342        renderer: &mut Renderer,
1343        theme: &Theme,
1344        _style: &Style,
1345        layout: Layout<'_>,
1346        cursor: mouse::Cursor,
1347        _viewport: &Rectangle,
1348    ) {
1349        let style = theme.style(&self.class);
1350        let bounds = layout.bounds();
1351        let plot_bounds = self.get_plot_layout(layout).bounds();
1352        let screen_rect = ScreenRect {
1353            x: plot_bounds.x,
1354            y: plot_bounds.y,
1355            width: plot_bounds.width,
1356            height: plot_bounds.height,
1357        };
1358
1359        // Retrieve the Memory from the Tree directly
1360        let memory = tree
1361            .state
1362            .downcast_ref::<Memory<AxisId, Message, Tag, Renderer>>();
1363
1364        // Get cache from memory
1365        let Some(mut cache) = memory.get_cache_mut() else {
1366            return;
1367        };
1368
1369        // Draw axes
1370        for (i, (_, axis)) in self.state.axes().iter().enumerate() {
1371            // We only care about layout bounds here to determine position
1372            let axis_layout = layout.children().nth(i).unwrap();
1373
1374            // Draw the axis itself (Ticks, labels, spine and gridlines)
1375            axis.draw::<Renderer>(
1376                renderer,
1377                theme,
1378                &style,
1379                axis_layout,
1380                &plot_bounds,
1381                &mut cache,
1382                &bounds,
1383            );
1384        }
1385
1386        // Connect axis spines
1387        self.draw_spine_corners(layout, &style, plot_bounds, renderer);
1388
1389        // Draw data layers if the cache needs redraw
1390        if cache.needs_redraw() {
1391            let mut interactions = memory.interaction_cache.borrow_mut();
1392            interactions.clear();
1393
1394            for layer in &self.layers {
1395                // These axes are guaranteed to exist because of `verify_layer` check
1396                let x_axis = self.state.axis(&layer.horizontal_axis_id);
1397                let y_axis = self.state.axis(&layer.vertical_axis_id);
1398                let transform = Transform::new(&screen_rect, x_axis.deref(), y_axis.deref());
1399
1400                let mut plot: Plot<Domain, Message, Tag, Renderer> =
1401                    Plot::new(renderer, &mut cache, &transform, &mut interactions);
1402
1403                // User code draws shapes into the plot here
1404                layer.items.draw(&mut plot, theme);
1405            }
1406
1407            // Populate the debug cache
1408            if self.debug
1409                && let Some(debug_cache_cell) = &memory.debug_cache
1410            {
1411                let mut debug_cache = debug_cache_cell.borrow_mut();
1412
1413                // Safely recreate the mesh to clear old debug primitives
1414                let mut new_debug_cache = match renderer.preferred_backend() {
1415                    crate::render::Backend::Mesh => crate::render::RenderCache::new_mesh(),
1416                    crate::render::Backend::Path => crate::render::RenderCache::new_path(),
1417                };
1418                new_debug_cache.set_quality(self.quality);
1419
1420                for (_, interaction) in interactions.iter() {
1421                    if let Some(primitive) = build_debug_primitive(&interaction.area) {
1422                        new_debug_cache.add_primitive(primitive);
1423                    }
1424                }
1425
1426                // Assign the fresh cache
1427                *debug_cache = new_debug_cache;
1428            }
1429        }
1430
1431        // Draw markers
1432        for marker_request in &self.markers {
1433            let Some((idx, _id, axis)) = self.state.axes().get_full(marker_request.axis_id) else {
1434                continue;
1435            };
1436
1437            let axis_bounds = layout.child(idx).bounds();
1438
1439            let Some((marker, normalized_position)) = marker_request.create_marker(
1440                axis,
1441                &axis_bounds,
1442                &plot_bounds,
1443                cursor,
1444                &style.axis,
1445                theme,
1446            ) else {
1447                continue;
1448            };
1449
1450            axis.draw_marker_overlay(
1451                renderer,
1452                normalized_position,
1453                marker,
1454                axis_bounds,
1455                &bounds,
1456                style.axis.text_offset,
1457            );
1458        }
1459
1460        // DEBUG!
1461        // memory.draw_partitions(renderer, plot_bounds);
1462
1463        // Draw the currently cached primitives
1464        cache.draw(renderer, &plot_bounds);
1465
1466        // Draw exact mathematical shape interactions if debug is enabled
1467        if self.debug
1468            && let Some(mut debug_cache) = memory.get_debug_cache_mut()
1469        {
1470            debug_cache.draw(renderer, &plot_bounds);
1471        }
1472
1473        // --- INTERACTION DEBUG VISUALIZER ---
1474        if self.debug {
1475            for (_, interaction) in memory.interaction_cache.borrow().iter() {
1476                // Only draw the intersection of the bounding box and the plot area
1477                if let Some(clipped_bounds) = interaction.bounding_box.intersection(&plot_bounds) {
1478                    renderer.fill_quad(
1479                        Quad {
1480                            bounds: clipped_bounds,
1481                            border: iced_core::Border::default()
1482                                .color(Color::from_rgba(1.0, 0.0, 0.0, 0.8))
1483                                .width(1.0),
1484                            ..Default::default()
1485                        },
1486                        Color::from_rgba(1.0, 0.0, 0.0, 0.1),
1487                    );
1488                }
1489            }
1490        }
1491    }
1492    fn mouse_interaction(
1493        &self,
1494        tree: &Tree,
1495        layout: Layout<'_>,
1496        cursor: mouse::Cursor,
1497        _viewport: &Rectangle,
1498        _renderer: &Renderer,
1499    ) -> mouse::Interaction {
1500        let memory: &Memory<AxisId, Message, Tag, Renderer> = tree.state.downcast_ref();
1501        let interactions = memory.interaction_cache.borrow();
1502
1503        // 1. Identify if we are interacting with a specific shape
1504        let (target_id, click_kind, button_held, drag_delta) = match &memory.action {
1505            Action::DraggingPlot {
1506                interaction_id,
1507                click_kind,
1508                button,
1509                total_delta,
1510                ..
1511            } => (
1512                interaction_id.clone(),
1513                Some(*click_kind),
1514                Some(*button),
1515                Some(*total_delta),
1516            ),
1517            Action::Idle => {
1518                let id = if let HoverIdentity::Interaction(id) = &memory.last_hovered_identity {
1519                    Some(id.clone())
1520                } else {
1521                    None
1522                };
1523                (id, None, None, None)
1524            }
1525            _ => (None, None, None, None),
1526        };
1527
1528        // 2. Query for a Specific Interaction Cursor
1529        if let Some(id) = target_id
1530            && let Some(interaction) = interactions.get(&id)
1531            && let Some(handler) = interaction.cursor_handler.as_ref()
1532        {
1533            // Calculate mutually exclusive states
1534            let is_dragging = drag_delta.is_some_and(|delta| delta >= self.drag_deadband);
1535            let is_pressed = drag_delta.is_some_and(|_| !is_dragging);
1536            let is_hovered = !is_pressed
1537                && !is_dragging
1538                && matches!(memory.last_hovered_identity, HoverIdentity::Interaction(ref i) if i == &id);
1539
1540            let status = interaction::InteractionStatus {
1541                is_hovered,
1542                is_pressed,
1543                is_dragging,
1544                button_held,
1545                click_kind,
1546                modifiers: memory.keyboard_modifiers,
1547            };
1548
1549            // If the user provided a specific cursor, return it immediately
1550            if let Some(preferred_cursor) = handler.run((status,)) {
1551                return preferred_cursor;
1552            }
1553        }
1554
1555        // 3. System Default Fallback
1556        // If the mouse is over the plot bounds, use the user's custom background cursor.
1557        // Otherwise, if outside the chart entirely, return the standard arrow.
1558        if cursor.position_over(layout.bounds()).is_some() {
1559            self.default_cursor
1560        } else {
1561            mouse::Interaction::Idle
1562        }
1563    }
1564}
1565
1566// Boilerplate conversions and helpers
1567
1568impl<'a, AxisId, Domain, Message, Tag, Theme, Renderer>
1569    From<Chart<'a, AxisId, Domain, Message, Tag, Theme, Renderer>>
1570    for Element<'a, Message, Theme, Renderer>
1571where
1572    AxisId: Hash + Eq + Debug + Clone + 'static,
1573    Domain: Float + 'static,
1574    Message: Clone + 'a + 'static,
1575    Theme: Catalog + 'a,
1576    Renderer: crate::Renderer + iced_core::text::Renderer<Font = iced_core::Font> + 'static,
1577    Tag: Hash + Eq + Clone + Debug + 'static,
1578{
1579    fn from(plot: Chart<'a, AxisId, Domain, Message, Tag, Theme, Renderer>) -> Self {
1580        Element::new(plot)
1581    }
1582}
1583
1584#[inline(always)]
1585fn layout_horizontal_axis<Domain: Float, Theme>(
1586    chart_width: f32,
1587    axis: &Axis<Domain, Theme>,
1588    x: f32,
1589    y: f32,
1590    height: f32,
1591) -> Node {
1592    let limits = Limits::new(
1593        Size::new(chart_width, height),
1594        Size::new(chart_width, height),
1595    );
1596    axis.layout(&limits).move_to(Point::new(x, y))
1597}
1598
1599#[inline(always)]
1600fn layout_vertical_axis<Domain: Float, Theme>(
1601    chart_height: f32,
1602    axis: &Axis<Domain, Theme>,
1603    x: f32,
1604    y: f32,
1605    width: f32,
1606) -> Node {
1607    let limits = Limits::new(
1608        Size::new(width, chart_height),
1609        Size::new(width, chart_height),
1610    );
1611    axis.layout(&limits).move_to(Point::new(x, y))
1612}
1613
1614#[inline(always)]
1615fn verify_layer<
1616    'a,
1617    AxisId: Hash + Eq + Clone,
1618    Domain: Float,
1619    Message,
1620    Tag: Hash + Eq + Clone,
1621    Renderer,
1622    Theme,
1623>(
1624    layer: &Layer<'a, AxisId, Domain, Message, Tag, Renderer, Theme>,
1625    state: &'a State<AxisId, Domain, Theme>,
1626    errors: &mut Vec<Error<AxisId>>,
1627) -> bool {
1628    let x_id = &layer.horizontal_axis_id;
1629    let y_id = &layer.vertical_axis_id;
1630
1631    if x_id == y_id {
1632        errors.push(Error::DuplicateAxis { id: x_id.clone() });
1633        return false;
1634    }
1635
1636    let Some(x) = state.axis_opt(x_id) else {
1637        errors.push(Error::UnknownAxis { id: x_id.clone() });
1638        return false;
1639    };
1640    let Some(y) = state.axis_opt(y_id) else {
1641        errors.push(Error::UnknownAxis { id: y_id.clone() });
1642        return false;
1643    };
1644
1645    let horizontal_orientation = x.orientation();
1646    let vertical_orientation = y.orientation();
1647    if horizontal_orientation == vertical_orientation {
1648        errors.push(Error::AxisConflict {
1649            horizontal: x_id.clone(),
1650            horizontal_orientation,
1651            vertical: y_id.clone(),
1652            vertical_orientation,
1653        });
1654        return false;
1655    }
1656
1657    true
1658}
1659
1660/// Translates a mathematical interaction area into a renderable stroke outline.
1661pub(crate) fn build_debug_primitive(area: &Area) -> Option<Primitive> {
1662    // A standard 1px red stroke for our X-Ray lines
1663    let debug_stroke = ResolvedStroke {
1664        thickness: 1.0,
1665        fill: Color::from_rgba(1.0, 0.0, 0.0, 0.8),
1666        style: crate::stroke::StrokeStyle::Solid,
1667    };
1668
1669    match area {
1670        Area::Rectangle { top_left, size } => Some(Primitive::Rectangle {
1671            xy1: Point::new(top_left.x, top_left.y),
1672            xy2: Point::new(top_left.x + size.width, top_left.y + size.height),
1673            fill: None,
1674            stroke: Some(debug_stroke),
1675        }),
1676        Area::LineSegment {
1677            p1,
1678            p2,
1679            stroke_width,
1680        } => Some(Primitive::Line {
1681            start: *p1,
1682            end: *p2,
1683            stroke: ResolvedStroke {
1684                thickness: stroke_width.0,
1685                ..debug_stroke
1686            },
1687            clip_bounds: Rectangle::INFINITE, // Will be clipped by plot bounds later
1688            extensions: LineExtensions {
1689                start: false,
1690                end: false,
1691            },
1692            arrows: LineArrows {
1693                start: false,
1694                end: false,
1695                size: 0.0,
1696            },
1697        }),
1698        Area::Ellipse { center, radii } => Some(Primitive::Ellipse {
1699            center: *center,
1700            radii: *radii,
1701            fill: None,
1702            stroke: Some(debug_stroke),
1703        }),
1704        Area::Triangle { p1, p2, p3 } => Some(Primitive::Triangle {
1705            points: [*p1, *p2, *p3],
1706            fill: None,
1707            stroke: Some(debug_stroke),
1708        }),
1709        Area::Polygon { points } => Some(Primitive::Area {
1710            points: points.clone(),
1711            fill: None,
1712            stroke: Some(debug_stroke),
1713        }),
1714        Area::Polyline {
1715            points,
1716            stroke_width,
1717        } => Some(Primitive::PolyLine {
1718            points: points.clone(),
1719            stroke: ResolvedStroke {
1720                thickness: stroke_width.0,
1721                ..debug_stroke
1722            },
1723            clip_bounds: Rectangle::INFINITE,
1724            extensions: LineExtensions {
1725                start: false,
1726                end: false,
1727            },
1728            arrows: LineArrows {
1729                start: false,
1730                end: false,
1731                size: 0.0,
1732            },
1733        }),
1734        Area::RegularPolygon {
1735            center,
1736            radius,
1737            vertices,
1738            rotation,
1739        } => Some(Primitive::Polygon {
1740            center: *center,
1741            radius: *radius,
1742            vertices: *vertices,
1743            rotation: *rotation,
1744            fill: None,
1745            stroke: Some(debug_stroke),
1746        }),
1747        Area::Arc {
1748            center,
1749            radius_outer,
1750            radius_inner,
1751            start_angle,
1752            end_angle,
1753        } => Some(Primitive::Arc {
1754            center: *center,
1755            radius_inner: Some(*radius_inner),
1756            radius_outer: *radius_outer,
1757            start_angle: *start_angle,
1758            end_angle: *end_angle,
1759            fill: None,
1760            stroke: Some(debug_stroke),
1761        }),
1762        Area::Custom(_) => None, // Cannot easily draw custom dynamic interactions
1763    }
1764}