Skip to main content

egui_plot/items/
span.rs

1use std::f32::consts::PI;
2use std::ops::RangeInclusive;
3
4use egui::Align2;
5use egui::Color32;
6use egui::Pos2;
7use egui::Rect;
8use egui::Shape;
9use egui::Stroke;
10use egui::TextStyle;
11use egui::Ui;
12use egui::Vec2;
13use egui::epaint::PathStroke;
14use egui::epaint::TextShape;
15use egui::pos2;
16use emath::TSTransform;
17
18use crate::aesthetics::LineStyle;
19use crate::axis::Axis;
20use crate::axis::PlotTransform;
21use crate::bounds::PlotBounds;
22use crate::bounds::PlotPoint;
23use crate::colors::highlighted_color;
24use crate::items::PlotGeometry;
25use crate::items::PlotItem;
26use crate::items::PlotItemBase;
27use crate::utils::find_name_candidate;
28
29/// Padding between the label of the span and both the edge of the view and the
30/// span borders.
31///
32/// For example, for a horizontal span, this is the padding between the top of
33/// the span label and the top edge of the plot view, but also the margin
34/// between the left/right edges of the span and the span label.
35const LABEL_PADDING: f32 = 4.0;
36
37/// A span covering a range on either axis.
38#[derive(Clone, Debug, PartialEq)]
39pub struct Span {
40    base: PlotItemBase,
41    axis: Axis,
42    range: RangeInclusive<f64>,
43    fill: Color32,
44    border_stroke: Stroke,
45    border_style: LineStyle,
46    label_align: Align2,
47}
48
49impl Span {
50    /// Create a new span covering the provided range on the X axis by default.
51    pub fn new(name: impl Into<String>, range: impl Into<RangeInclusive<f64>>) -> Self {
52        Self {
53            base: PlotItemBase::new(name.into()),
54            axis: Axis::X,
55            range: range.into(),
56            fill: Color32::TRANSPARENT,
57            border_stroke: Stroke::new(1.0, Color32::TRANSPARENT),
58            border_style: LineStyle::Solid,
59            label_align: Align2::CENTER_TOP,
60        }
61    }
62
63    /// Select which axis the span applies to. This also sets the label
64    /// alignment. If you want a different label alignment, you need to set
65    /// it by calling `label_align` after this call.
66    #[inline]
67    pub fn axis(mut self, axis: Axis) -> Self {
68        self.axis = axis;
69        match axis {
70            Axis::X => self.label_align = Align2::CENTER_TOP,
71            Axis::Y => self.label_align = Align2::LEFT_CENTER,
72        }
73        self
74    }
75
76    /// Set the range.
77    #[inline]
78    pub fn range(mut self, range: impl Into<RangeInclusive<f64>>) -> Self {
79        self.range = range.into();
80        self
81    }
82
83    /// Set the background fill color for the span.
84    #[inline]
85    pub fn fill(mut self, color: impl Into<Color32>) -> Self {
86        self.fill = color.into();
87        self
88    }
89
90    /// Set the stroke used for both span borders.
91    #[inline]
92    pub fn border(mut self, stroke: impl Into<Stroke>) -> Self {
93        self.border_stroke = stroke.into();
94        self
95    }
96
97    /// Convenience for updating the span border width.
98    #[inline]
99    pub fn border_width(mut self, width: impl Into<f32>) -> Self {
100        self.border_stroke.width = width.into();
101        self
102    }
103
104    /// Convenience for updating the span border color.
105    #[inline]
106    pub fn border_color(mut self, color: impl Into<Color32>) -> Self {
107        self.border_stroke.color = color.into();
108        self
109    }
110
111    /// Set the style for the span borders. Defaults to `LineStyle::Solid`.
112    #[inline]
113    pub fn border_style(mut self, style: LineStyle) -> Self {
114        self.border_style = style;
115        self
116    }
117
118    /// Set the label alignment within the span.
119    /// This should be called after any calls to `axis` as that would overwrite
120    /// the label alignment
121    #[inline]
122    pub fn label_align(mut self, align: Align2) -> Self {
123        self.label_align = align;
124        self
125    }
126
127    #[inline]
128    pub(crate) fn fill_color(&self) -> Color32 {
129        self.fill
130    }
131
132    #[inline]
133    pub(crate) fn border_color_value(&self) -> Color32 {
134        self.border_stroke.color
135    }
136
137    fn range_sorted(&self) -> (f64, f64) {
138        let start = *self.range.start();
139        let end = *self.range.end();
140        if start <= end { (start, end) } else { (end, start) }
141    }
142
143    fn hline_points(value: f64, transform: &PlotTransform) -> Vec<Pos2> {
144        vec![
145            transform.position_from_point(&PlotPoint::new(transform.bounds().min[0], value)),
146            transform.position_from_point(&PlotPoint::new(transform.bounds().max[0], value)),
147        ]
148    }
149
150    fn vline_points(value: f64, transform: &PlotTransform) -> Vec<Pos2> {
151        vec![
152            transform.position_from_point(&PlotPoint::new(value, transform.bounds().min[1])),
153            transform.position_from_point(&PlotPoint::new(value, transform.bounds().max[1])),
154        ]
155    }
156
157    fn draw_border(&self, value: f64, stroke: Stroke, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
158        if stroke.color == Color32::TRANSPARENT || stroke.width <= 0.0 || !value.is_finite() {
159            return;
160        }
161
162        let line = match self.axis {
163            Axis::X => Self::vline_points(value, transform),
164            Axis::Y => Self::hline_points(value, transform),
165        };
166
167        self.border_style
168            .style_line(line, PathStroke::new(stroke.width, stroke.color), false, shapes);
169    }
170
171    fn available_width_for_name(&self, rect: &Rect) -> f32 {
172        match self.axis {
173            Axis::X => (rect.width() - 2.0 * LABEL_PADDING).max(0.0),
174            Axis::Y => (rect.height() - 2.0 * LABEL_PADDING).max(0.0),
175        }
176    }
177
178    fn draw_name(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>, span_rect: &Rect) {
179        let frame = *transform.frame();
180        let visible_rect = span_rect.intersect(frame);
181
182        let available_width = self.available_width_for_name(&visible_rect);
183        if available_width <= 0.0 {
184            return;
185        }
186
187        let font_id = TextStyle::Body.resolve(ui.style());
188        let text_color = ui.visuals().text_color();
189        let painter = ui.painter();
190
191        let name = find_name_candidate(&self.base.name, available_width, painter, &font_id);
192
193        let galley = painter.layout_no_wrap(name, font_id, text_color);
194
195        if galley.is_empty() {
196            return;
197        }
198
199        // Place text center point at origin and rotate for Y-axis.
200        let mut text_shape = match self.axis {
201            Axis::X => TextShape::new(pos2(-galley.size().x / 2.0, -galley.size().y / 2.0), galley, text_color),
202
203            // For spans on the Y axis we rotate the text by 90° around its center point
204            Axis::Y => TextShape::new(pos2(-galley.size().x / 2.0, -galley.size().y / 2.0), galley, text_color)
205                .with_angle_and_anchor(-PI / 2.0, Align2::CENTER_CENTER),
206        };
207
208        // Take into account the rotation of the text when calculating its position
209        let text_rect = text_shape.visual_bounding_rect();
210        let (width, height) = (text_rect.width(), text_rect.height());
211
212        // Calculate the position of the text based on the label alignment
213        let text_pos_x = match self.label_align {
214            Align2::LEFT_CENTER | Align2::LEFT_TOP | Align2::LEFT_BOTTOM => visible_rect.left() + LABEL_PADDING,
215            Align2::CENTER_CENTER | Align2::CENTER_TOP | Align2::CENTER_BOTTOM => visible_rect.center().x - width / 2.0,
216            Align2::RIGHT_CENTER | Align2::RIGHT_TOP | Align2::RIGHT_BOTTOM => {
217                visible_rect.right() - LABEL_PADDING - width
218            }
219        };
220
221        let text_pos_y = match self.label_align {
222            Align2::LEFT_TOP | Align2::CENTER_TOP | Align2::RIGHT_TOP => visible_rect.top() + LABEL_PADDING,
223            Align2::LEFT_CENTER | Align2::CENTER_CENTER | Align2::RIGHT_CENTER => {
224                visible_rect.center().y - height / 2.0
225            }
226            Align2::LEFT_BOTTOM | Align2::CENTER_BOTTOM | Align2::RIGHT_BOTTOM => {
227                visible_rect.bottom() - LABEL_PADDING - height
228            }
229        };
230
231        // Make sure to add half the width/height since the text position is at the
232        // center of the text shape
233        let text_pos = pos2(text_pos_x + width / 2.0, text_pos_y + height / 2.0);
234
235        text_shape.transform(TSTransform::from_translation(Vec2::new(text_pos.x, text_pos.y)));
236
237        shapes.push(text_shape.into());
238    }
239}
240
241impl PlotItem for Span {
242    fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
243        let plot_bounds = match self.axis {
244            Axis::X => transform.bounds().range_x(),
245            Axis::Y => transform.bounds().range_y(),
246        };
247
248        let (range_min, range_max) = self.range_sorted();
249
250        // If the span is outside of the visible range, don't draw anything.
251        if range_max < *plot_bounds.start() || range_min > *plot_bounds.end() {
252            return;
253        }
254
255        let mut stroke = self.border_stroke;
256        let mut fill = self.fill;
257        if self.base.highlight {
258            (stroke, fill) = highlighted_color(stroke, fill);
259        }
260
261        // Clamp the range to support (half-)infinite spans
262        let range_min_clamped = range_min.max(*plot_bounds.start());
263        let range_max_clamped = range_max.min(*plot_bounds.end());
264
265        // Draw the rect first with the clamped range
266        let span_rect = match self.axis {
267            Axis::X => transform.rect_from_values(
268                &PlotPoint::new(range_min_clamped, transform.bounds().min[1]),
269                &PlotPoint::new(range_max_clamped, transform.bounds().max[1]),
270            ),
271            Axis::Y => transform.rect_from_values(
272                &PlotPoint::new(transform.bounds().min[0], range_min_clamped),
273                &PlotPoint::new(transform.bounds().max[0], range_max_clamped),
274            ),
275        };
276
277        if fill != Color32::TRANSPARENT && span_rect.is_positive() {
278            shapes.push(Shape::rect_filled(span_rect, 0.0, fill));
279        }
280
281        // Draw the first border if it is in bounds
282        if plot_bounds.contains(&range_min) {
283            self.draw_border(range_min, stroke, transform, shapes);
284        }
285
286        // Draw the second border if it is in bounds
287        if plot_bounds.contains(&range_max) {
288            self.draw_border(range_max, stroke, transform, shapes);
289        }
290
291        self.draw_name(ui, transform, shapes, &span_rect);
292    }
293
294    fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
295
296    fn color(&self) -> Color32 {
297        if self.fill != Color32::TRANSPARENT {
298            self.fill
299        } else {
300            self.border_stroke.color
301        }
302    }
303
304    fn geometry(&self) -> PlotGeometry<'_> {
305        PlotGeometry::None
306    }
307
308    fn bounds(&self) -> PlotBounds {
309        PlotBounds::NOTHING
310    }
311
312    fn base(&self) -> &PlotItemBase {
313        &self.base
314    }
315
316    fn base_mut(&mut self) -> &mut PlotItemBase {
317        &mut self.base
318    }
319}