embedded_charts/legend/
traits.rs

1//! Core traits for legend implementations.
2
3use crate::error::ChartResult;
4use embedded_graphics::{prelude::*, primitives::Rectangle};
5
6#[cfg(feature = "std")]
7use std::vec::Vec;
8
9#[cfg(all(feature = "no_std", not(feature = "std")))]
10extern crate alloc;
11
12#[cfg(all(feature = "no_std", not(feature = "std")))]
13use alloc::vec::Vec;
14
15/// Main trait for legend implementations
16pub trait Legend<C: PixelColor> {
17    /// The type of legend entries this legend contains
18    type Entry: LegendEntry<C>;
19
20    /// Get all legend entries
21    fn entries(&self) -> &[Self::Entry];
22
23    /// Get mutable access to legend entries
24    fn entries_mut(&mut self) -> &mut [Self::Entry];
25
26    /// Add a new entry to the legend
27    fn add_entry(&mut self, entry: Self::Entry) -> ChartResult<()>;
28
29    /// Remove an entry by index
30    fn remove_entry(&mut self, index: usize) -> ChartResult<()>;
31
32    /// Clear all entries
33    fn clear_entries(&mut self);
34
35    /// Get the legend position
36    fn position(&self) -> crate::legend::position::LegendPosition;
37
38    /// Set the legend position
39    fn set_position(&mut self, position: crate::legend::position::LegendPosition);
40
41    /// Get the legend orientation
42    fn orientation(&self) -> crate::legend::types::LegendOrientation;
43
44    /// Set the legend orientation
45    fn set_orientation(&mut self, orientation: crate::legend::types::LegendOrientation);
46
47    /// Calculate the required size for this legend
48    fn calculate_size(&self) -> Size;
49
50    /// Check if the legend is empty
51    fn is_empty(&self) -> bool {
52        self.entries().is_empty()
53    }
54
55    /// Get the number of visible entries
56    fn visible_entry_count(&self) -> usize {
57        self.entries().iter().filter(|e| e.is_visible()).count()
58    }
59}
60
61/// Trait for rendering legends to a display target
62pub trait LegendRenderer<C: PixelColor> {
63    /// The legend type this renderer can handle
64    type Legend: Legend<C>;
65
66    /// Render the legend to the target display
67    ///
68    /// # Arguments
69    /// * `legend` - The legend to render
70    /// * `viewport` - The area to render the legend in
71    /// * `target` - The display target to render to
72    fn render<D>(
73        &self,
74        legend: &Self::Legend,
75        viewport: Rectangle,
76        target: &mut D,
77    ) -> ChartResult<()>
78    where
79        D: DrawTarget<Color = C>;
80
81    /// Calculate the layout for legend entries within the viewport
82    ///
83    /// # Arguments
84    /// * `legend` - The legend to calculate layout for
85    /// * `viewport` - The available area for the legend
86    fn calculate_layout(
87        &self,
88        legend: &Self::Legend,
89        viewport: Rectangle,
90    ) -> ChartResult<heapless::Vec<Rectangle, 8>>;
91
92    /// Render a single legend entry
93    ///
94    /// # Arguments
95    /// * `entry` - The legend entry to render
96    /// * `bounds` - The area to render the entry in
97    /// * `target` - The display target to render to
98    fn render_entry<D>(
99        &self,
100        entry: &<Self::Legend as Legend<C>>::Entry,
101        bounds: Rectangle,
102        target: &mut D,
103    ) -> ChartResult<()>
104    where
105        D: DrawTarget<Color = C>;
106}
107
108/// Trait for individual legend entries
109pub trait LegendEntry<C: PixelColor> {
110    /// Get the label text for this entry
111    fn label(&self) -> &str;
112
113    /// Set the label text for this entry
114    fn set_label(&mut self, label: &str) -> ChartResult<()>;
115
116    /// Get the entry type (determines the symbol)
117    fn entry_type(&self) -> &crate::legend::types::LegendEntryType<C>;
118
119    /// Set the entry type
120    fn set_entry_type(&mut self, entry_type: crate::legend::types::LegendEntryType<C>);
121
122    /// Check if this entry is visible
123    fn is_visible(&self) -> bool;
124
125    /// Set the visibility of this entry
126    fn set_visible(&mut self, visible: bool);
127
128    /// Calculate the required size for this entry
129    fn calculate_size(&self, style: &crate::legend::style::LegendStyle<C>) -> Size;
130
131    /// Render the symbol for this entry
132    fn render_symbol<D>(
133        &self,
134        bounds: Rectangle,
135        style: &crate::legend::style::SymbolStyle<C>,
136        target: &mut D,
137    ) -> ChartResult<()>
138    where
139        D: DrawTarget<Color = C>;
140}
141
142/// Trait for legends that can automatically generate entries from chart data
143pub trait AutoLegend<C: PixelColor>: Legend<C> {
144    /// The type of data series this legend can generate entries for
145    type DataSeries;
146
147    /// Generate legend entries from data series
148    fn generate_from_series(&mut self, series: &[Self::DataSeries]) -> ChartResult<()>;
149
150    /// Generate a single entry from a data series
151    fn generate_entry_from_series(
152        &self,
153        series: &Self::DataSeries,
154        index: usize,
155    ) -> ChartResult<Self::Entry>;
156
157    /// Update existing entries to match current data series
158    fn update_from_series(&mut self, series: &[Self::DataSeries]) -> ChartResult<()>;
159}
160
161/// Trait for legends that support interactive features
162pub trait InteractiveLegend<C: PixelColor>: Legend<C> {
163    /// Event type for legend interactions
164    type Event;
165    /// Response type for legend interactions
166    type Response;
167
168    /// Handle an interaction event
169    ///
170    /// # Arguments
171    /// * `event` - The interaction event
172    /// * `viewport` - The legend viewport
173    fn handle_event(
174        &mut self,
175        event: Self::Event,
176        viewport: Rectangle,
177    ) -> ChartResult<Self::Response>;
178
179    /// Check if a point is within a legend entry
180    ///
181    /// # Arguments
182    /// * `point` - The point to check
183    /// * `viewport` - The legend viewport
184    fn hit_test(&self, point: Point, viewport: Rectangle) -> Option<usize>;
185
186    /// Toggle the visibility of an entry
187    fn toggle_entry(&mut self, index: usize) -> ChartResult<()>;
188
189    /// Get the currently selected entry index
190    fn selected_entry(&self) -> Option<usize>;
191
192    /// Set the selected entry
193    fn set_selected_entry(&mut self, index: Option<usize>);
194}
195
196/// Default legend renderer implementation
197#[derive(Debug, Clone)]
198pub struct DefaultLegendRenderer<C: PixelColor> {
199    _phantom: core::marker::PhantomData<C>,
200}
201
202impl<C: PixelColor> DefaultLegendRenderer<C> {
203    /// Create a new default legend renderer
204    pub fn new() -> Self {
205        Self {
206            _phantom: core::marker::PhantomData,
207        }
208    }
209}
210
211impl<C: PixelColor> Default for DefaultLegendRenderer<C> {
212    fn default() -> Self {
213        Self::new()
214    }
215}
216
217impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> LegendRenderer<C>
218    for DefaultLegendRenderer<C>
219{
220    type Legend = crate::legend::DefaultLegend<C>;
221
222    fn render<D>(
223        &self,
224        legend: &Self::Legend,
225        viewport: Rectangle,
226        target: &mut D,
227    ) -> ChartResult<()>
228    where
229        D: DrawTarget<Color = C>,
230    {
231        if legend.entries.is_empty() {
232            return Ok(());
233        }
234
235        let entry_bounds = self.calculate_layout(legend, viewport)?;
236
237        // Render background if configured
238        if let Some(bg_color) = legend.style.background.color {
239            use embedded_graphics::primitives::PrimitiveStyle;
240            use embedded_graphics::primitives::Rectangle as EgRectangle;
241
242            EgRectangle::new(viewport.top_left, viewport.size)
243                .into_styled(PrimitiveStyle::with_fill(bg_color))
244                .draw(target)
245                .map_err(|_| crate::error::ChartError::RenderingError)?;
246        }
247
248        // Render each visible entry
249        for (entry, bounds) in legend
250            .entries
251            .iter()
252            .filter(|e| e.visible)
253            .zip(entry_bounds.iter())
254        {
255            self.render_entry(entry, *bounds, target)?;
256        }
257
258        Ok(())
259    }
260
261    fn calculate_layout(
262        &self,
263        legend: &Self::Legend,
264        viewport: Rectangle,
265    ) -> ChartResult<heapless::Vec<Rectangle, 8>> {
266        let mut layouts = heapless::Vec::new();
267        let visible_entries: Vec<_> = legend.entries.iter().filter(|e| e.visible).collect();
268
269        if visible_entries.is_empty() {
270            return Ok(layouts);
271        }
272
273        match legend.orientation {
274            crate::legend::types::LegendOrientation::Vertical => {
275                let entry_height = legend.style.text.line_height;
276                let spacing = legend.style.spacing.entry_spacing;
277
278                for (i, _) in visible_entries.iter().enumerate() {
279                    let y_offset = i as u32 * (entry_height + spacing);
280                    let bounds = Rectangle::new(
281                        Point::new(viewport.top_left.x, viewport.top_left.y + y_offset as i32),
282                        Size::new(viewport.size.width, entry_height),
283                    );
284                    if layouts.push(bounds).is_err() {
285                        return Err(crate::error::ChartError::ConfigurationError);
286                    }
287                }
288            }
289            crate::legend::types::LegendOrientation::Horizontal => {
290                let mut x_offset = 0u32;
291                let entry_height = legend.style.text.line_height;
292
293                for entry in visible_entries.iter() {
294                    let entry_width = legend.style.spacing.symbol_width
295                        + legend.style.spacing.symbol_text_gap
296                        + entry.label.len() as u32 * legend.style.text.char_width;
297
298                    let bounds = Rectangle::new(
299                        Point::new(viewport.top_left.x + x_offset as i32, viewport.top_left.y),
300                        Size::new(entry_width, entry_height),
301                    );
302                    if layouts.push(bounds).is_err() {
303                        return Err(crate::error::ChartError::ConfigurationError);
304                    }
305
306                    x_offset += entry_width + legend.style.spacing.entry_spacing;
307                }
308            }
309        }
310
311        /// Standard legend renderer implementation
312        #[derive(Debug, Clone)]
313        pub struct StandardLegendRenderer<C: PixelColor> {
314            _phantom: core::marker::PhantomData<C>,
315        }
316
317        impl<C: PixelColor> StandardLegendRenderer<C> {
318            /// Create a new standard legend renderer
319            pub fn new() -> Self {
320                Self {
321                    _phantom: core::marker::PhantomData,
322                }
323            }
324        }
325
326        impl<C: PixelColor> Default for StandardLegendRenderer<C> {
327            fn default() -> Self {
328                Self::new()
329            }
330        }
331
332        impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> LegendRenderer<C>
333            for StandardLegendRenderer<C>
334        {
335            type Legend = crate::legend::types::StandardLegend<C>;
336
337            fn render<D>(
338                &self,
339                legend: &Self::Legend,
340                viewport: Rectangle,
341                target: &mut D,
342            ) -> ChartResult<()>
343            where
344                D: DrawTarget<Color = C>,
345            {
346                if legend.entries().is_empty() {
347                    return Ok(());
348                }
349
350                let entry_bounds = self.calculate_layout(legend, viewport)?;
351
352                // Render background if configured
353                if let Some(bg_color) = legend.style().background.color {
354                    use embedded_graphics::primitives::PrimitiveStyle;
355                    use embedded_graphics::primitives::Rectangle as EgRectangle;
356
357                    EgRectangle::new(viewport.top_left, viewport.size)
358                        .into_styled(PrimitiveStyle::with_fill(bg_color))
359                        .draw(target)
360                        .map_err(|_| crate::error::ChartError::RenderingError)?;
361                }
362
363                // Render each visible entry
364                for (entry, bounds) in legend
365                    .entries()
366                    .iter()
367                    .filter(|e| e.is_visible())
368                    .zip(entry_bounds.iter())
369                {
370                    self.render_entry(entry, *bounds, target)?;
371                }
372
373                Ok(())
374            }
375
376            fn calculate_layout(
377                &self,
378                legend: &Self::Legend,
379                viewport: Rectangle,
380            ) -> ChartResult<heapless::Vec<Rectangle, 8>> {
381                let mut layouts = heapless::Vec::new();
382                let visible_entries: Vec<_> =
383                    legend.entries().iter().filter(|e| e.is_visible()).collect();
384
385                if visible_entries.is_empty() {
386                    return Ok(layouts);
387                }
388
389                match legend.orientation() {
390                    crate::legend::types::LegendOrientation::Vertical => {
391                        let entry_height = legend.style().text.line_height;
392                        let spacing = legend.style().spacing.entry_spacing;
393
394                        for (i, _) in visible_entries.iter().enumerate() {
395                            let y_offset = i as u32 * (entry_height + spacing);
396                            let bounds = Rectangle::new(
397                                Point::new(
398                                    viewport.top_left.x,
399                                    viewport.top_left.y + y_offset as i32,
400                                ),
401                                Size::new(viewport.size.width, entry_height),
402                            );
403                            if layouts.push(bounds).is_err() {
404                                return Err(crate::error::ChartError::ConfigurationError);
405                            }
406                        }
407                    }
408                    crate::legend::types::LegendOrientation::Horizontal => {
409                        let mut x_offset = 0u32;
410                        let entry_height = legend.style().text.line_height;
411
412                        for entry in visible_entries.iter() {
413                            let entry_width = legend.style().spacing.symbol_width
414                                + legend.style().spacing.symbol_text_gap
415                                + entry.label().len() as u32 * legend.style().text.char_width;
416
417                            let bounds = Rectangle::new(
418                                Point::new(
419                                    viewport.top_left.x + x_offset as i32,
420                                    viewport.top_left.y,
421                                ),
422                                Size::new(entry_width, entry_height),
423                            );
424                            if layouts.push(bounds).is_err() {
425                                return Err(crate::error::ChartError::ConfigurationError);
426                            }
427
428                            x_offset += entry_width + legend.style().spacing.entry_spacing;
429                        }
430                    }
431                }
432
433                Ok(layouts)
434            }
435
436            fn render_entry<D>(
437                &self,
438                entry: &crate::legend::types::StandardLegendEntry<C>,
439                bounds: Rectangle,
440                target: &mut D,
441            ) -> ChartResult<()>
442            where
443                D: DrawTarget<Color = C>,
444            {
445                // Render symbol
446                let symbol_bounds = Rectangle::new(
447                    bounds.top_left,
448                    Size::new(bounds.size.width.min(20), bounds.size.height),
449                );
450                entry.render_symbol(
451                    symbol_bounds,
452                    &crate::legend::style::SymbolStyle::default(),
453                    target,
454                )?;
455
456                // Render text (simplified - would need proper text rendering in full implementation)
457                // For now, we'll skip text rendering as it requires font support
458
459                Ok(())
460            }
461        }
462
463        Ok(layouts)
464    }
465
466    fn render_entry<D>(
467        &self,
468        entry: &crate::legend::DefaultLegendEntry<C>,
469        bounds: Rectangle,
470        target: &mut D,
471    ) -> ChartResult<()>
472    where
473        D: DrawTarget<Color = C>,
474    {
475        // Render symbol
476        let symbol_bounds = Rectangle::new(
477            bounds.top_left,
478            Size::new(bounds.size.width.min(20), bounds.size.height),
479        );
480        entry.render_symbol(
481            symbol_bounds,
482            &crate::legend::style::SymbolStyle::default(),
483            target,
484        )?;
485
486        // Render text label
487        let text_x = bounds.top_left.x + 25; // Symbol width + gap
488        let text_y = bounds.top_left.y + (bounds.size.height as i32 / 2);
489
490        // Use embedded-graphics text rendering
491        use embedded_graphics::{
492            mono_font::{ascii::FONT_6X10, MonoTextStyle},
493            text::{Baseline, Text},
494        };
495
496        let text_style = MonoTextStyle::new(
497            &FONT_6X10,
498            C::from(embedded_graphics::pixelcolor::Rgb565::BLACK),
499        );
500
501        Text::with_baseline(
502            entry.label(),
503            Point::new(text_x, text_y),
504            text_style,
505            Baseline::Middle,
506        )
507        .draw(target)
508        .map_err(|_| crate::error::ChartError::RenderingError)?;
509
510        Ok(())
511    }
512}
513
514/// Standard legend renderer implementation
515#[derive(Debug, Clone)]
516pub struct StandardLegendRenderer<C: PixelColor> {
517    _phantom: core::marker::PhantomData<C>,
518}
519
520impl<C: PixelColor> StandardLegendRenderer<C> {
521    /// Create a new standard legend renderer
522    pub fn new() -> Self {
523        Self {
524            _phantom: core::marker::PhantomData,
525        }
526    }
527}
528
529impl<C: PixelColor> Default for StandardLegendRenderer<C> {
530    fn default() -> Self {
531        Self::new()
532    }
533}
534
535impl<C: PixelColor + From<embedded_graphics::pixelcolor::Rgb565>> LegendRenderer<C>
536    for StandardLegendRenderer<C>
537{
538    type Legend = crate::legend::types::StandardLegend<C>;
539
540    fn render<D>(
541        &self,
542        legend: &Self::Legend,
543        viewport: Rectangle,
544        target: &mut D,
545    ) -> ChartResult<()>
546    where
547        D: DrawTarget<Color = C>,
548    {
549        if legend.entries().is_empty() {
550            return Ok(());
551        }
552
553        let entry_bounds = self.calculate_layout(legend, viewport)?;
554
555        // Render background if configured
556        if let Some(bg_color) = legend.style().background.color {
557            use embedded_graphics::primitives::PrimitiveStyle;
558            use embedded_graphics::primitives::Rectangle as EgRectangle;
559
560            EgRectangle::new(viewport.top_left, viewport.size)
561                .into_styled(PrimitiveStyle::with_fill(bg_color))
562                .draw(target)
563                .map_err(|_| crate::error::ChartError::RenderingError)?;
564        }
565
566        // Render each visible entry
567        for (entry, bounds) in legend
568            .entries()
569            .iter()
570            .filter(|e| e.is_visible())
571            .zip(entry_bounds.iter())
572        {
573            self.render_entry(entry, *bounds, target)?;
574        }
575
576        Ok(())
577    }
578
579    fn calculate_layout(
580        &self,
581        legend: &Self::Legend,
582        viewport: Rectangle,
583    ) -> ChartResult<heapless::Vec<Rectangle, 8>> {
584        let mut layouts = heapless::Vec::new();
585        let visible_entries: Vec<_> = legend.entries().iter().filter(|e| e.is_visible()).collect();
586
587        if visible_entries.is_empty() {
588            return Ok(layouts);
589        }
590
591        match legend.orientation() {
592            crate::legend::types::LegendOrientation::Vertical => {
593                let entry_height = legend.style().text.line_height;
594                let spacing = legend.style().spacing.entry_spacing;
595
596                for (i, _) in visible_entries.iter().enumerate() {
597                    let y_offset = i as u32 * (entry_height + spacing);
598                    let bounds = Rectangle::new(
599                        Point::new(viewport.top_left.x, viewport.top_left.y + y_offset as i32),
600                        Size::new(viewport.size.width, entry_height),
601                    );
602                    if layouts.push(bounds).is_err() {
603                        return Err(crate::error::ChartError::ConfigurationError);
604                    }
605                }
606            }
607            crate::legend::types::LegendOrientation::Horizontal => {
608                let mut x_offset = 0u32;
609                let entry_height = legend.style().text.line_height;
610
611                for entry in visible_entries.iter() {
612                    let entry_width = legend.style().spacing.symbol_width
613                        + legend.style().spacing.symbol_text_gap
614                        + entry.label().len() as u32 * legend.style().text.char_width;
615
616                    let bounds = Rectangle::new(
617                        Point::new(viewport.top_left.x + x_offset as i32, viewport.top_left.y),
618                        Size::new(entry_width, entry_height),
619                    );
620                    if layouts.push(bounds).is_err() {
621                        return Err(crate::error::ChartError::ConfigurationError);
622                    }
623
624                    x_offset += entry_width + legend.style().spacing.entry_spacing;
625                }
626            }
627        }
628
629        Ok(layouts)
630    }
631
632    fn render_entry<D>(
633        &self,
634        entry: &crate::legend::types::StandardLegendEntry<C>,
635        bounds: Rectangle,
636        target: &mut D,
637    ) -> ChartResult<()>
638    where
639        D: DrawTarget<Color = C>,
640    {
641        // Render symbol
642        let symbol_bounds = Rectangle::new(
643            bounds.top_left,
644            Size::new(bounds.size.width.min(20), bounds.size.height),
645        );
646        entry.render_symbol(
647            symbol_bounds,
648            &crate::legend::style::SymbolStyle::default(),
649            target,
650        )?;
651
652        // Render text label
653        let text_x = bounds.top_left.x + 25; // Symbol width + gap
654        let text_y = bounds.top_left.y + (bounds.size.height as i32 / 2);
655
656        // Use embedded-graphics text rendering
657        use embedded_graphics::{
658            mono_font::{ascii::FONT_6X10, MonoTextStyle},
659            text::{Baseline, Text},
660        };
661
662        let text_style = MonoTextStyle::new(
663            &FONT_6X10,
664            C::from(embedded_graphics::pixelcolor::Rgb565::BLACK),
665        );
666
667        Text::with_baseline(
668            entry.label(),
669            Point::new(text_x, text_y),
670            text_style,
671            Baseline::Middle,
672        )
673        .draw(target)
674        .map_err(|_| crate::error::ChartError::RenderingError)?;
675
676        Ok(())
677    }
678}