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