Skip to main content

libsurfer/
graphics.rs

1use ecolor::Color32;
2use emath::{Align, Align2, Vec2};
3use epaint::{CubicBezierShape, FontId, Shape, Stroke};
4use num::BigInt;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    config::SurferTheme, displayed_item::DisplayedItemRef, view::DrawingContext,
9    viewport::Viewport, wave_data::WaveData,
10};
11
12#[derive(Serialize, Deserialize, Debug)]
13pub enum Direction {
14    North,
15    East,
16    South,
17    West,
18}
19
20impl Direction {
21    #[must_use]
22    pub fn as_vector(&self) -> Vec2 {
23        match self {
24            Direction::North => Vec2::new(0., -1.),
25            Direction::East => Vec2::new(-1., 0.),
26            Direction::South => Vec2::new(0., 1.),
27            Direction::West => Vec2::new(1., 0.),
28        }
29    }
30}
31
32#[derive(Serialize, Deserialize, Debug, Clone)]
33pub enum Anchor {
34    Top,
35    Center,
36    Bottom,
37    Percentual(f32),
38}
39
40#[derive(Serialize, Deserialize, Debug, Clone)]
41pub struct GraphicsY {
42    pub item: DisplayedItemRef,
43    pub anchor: Anchor,
44}
45
46/// A point used to place graphics.
47#[derive(Serialize, Deserialize, Debug, Clone)]
48pub struct GrPoint {
49    /// Timestamp at which to place the graphic
50    pub x: BigInt,
51    pub y: GraphicsY,
52}
53
54#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord, Hash, Debug)]
55pub struct GraphicId(pub usize);
56
57#[derive(Serialize, Deserialize, Debug)]
58pub enum Graphic {
59    TextArrow {
60        from: (GrPoint, Direction),
61        to: (GrPoint, Direction),
62        text: String,
63    },
64    Text {
65        pos: (GrPoint, Direction),
66        text: String,
67    },
68    Rectangle {
69        from: GrPoint,
70        to: GrPoint,
71    },
72}
73
74impl WaveData {
75    // FIXME: This function should probably not be here, we should instead update ItemDrawingInfo to
76    // have this info
77    #[must_use]
78    pub fn get_item_y(&self, y: &GraphicsY) -> Option<f32> {
79        self.items_tree
80            .iter_visible()
81            .zip(&self.drawing_infos)
82            .find(|(node, _info)| node.item_ref == y.item)
83            .map(|(_, info)| match y.anchor {
84                Anchor::Top => info.top(),
85                Anchor::Center => info.top() + (info.bottom() - info.top()) / 2.,
86                Anchor::Percentual(p) => info.top() + p * (info.bottom() - info.top()),
87                Anchor::Bottom => info.bottom(),
88            })
89            .map(|point| point - self.top_item_draw_offset)
90    }
91
92    /// Returns the y-value of an item given a percentual value
93    #[must_use]
94    pub fn get_item_y_scale(&self, item: DisplayedItemRef, y: f32) -> Option<f32> {
95        let y = y + self.top_item_draw_offset;
96        self.items_tree
97            .iter_visible()
98            .zip(&self.drawing_infos)
99            .find(|(node, _info)| node.item_ref == item)
100            .map(|(_, info)| (y - info.top()) / (info.bottom() - info.top()))
101    }
102
103    pub(crate) fn draw_graphics(
104        &self,
105        ctx: &mut DrawingContext,
106        viewport: &Viewport,
107        theme: &SurferTheme,
108    ) {
109        let color = theme.variable_dontcare;
110        let num_timestamps = self.safe_num_timestamps();
111        for g in self.graphics.values() {
112            match g {
113                Graphic::TextArrow {
114                    from: (from_point, from_dir),
115                    to: (to_point, to_dir),
116                    text,
117                } => {
118                    let from_x = viewport.pixel_from_time(
119                        &from_point.x,
120                        ctx.cfg.canvas_size.x,
121                        &num_timestamps,
122                    );
123                    let from_y = self.get_item_y(&from_point.y);
124
125                    let to_x = viewport.pixel_from_time(
126                        &to_point.x,
127                        ctx.cfg.canvas_size.x,
128                        &num_timestamps,
129                    );
130                    let to_y = self.get_item_y(&to_point.y);
131
132                    if let (Some(from_y), Some(to_y)) = (from_y, to_y) {
133                        let from_dir = from_dir.as_vector() * 30.;
134                        let to_dir_vec = to_dir.as_vector() * 30.;
135                        let shape = Shape::CubicBezier(CubicBezierShape {
136                            points: [
137                                (ctx.to_screen)(from_x, from_y),
138                                (ctx.to_screen)(from_x + from_dir.x, from_y + from_dir.y),
139                                (ctx.to_screen)(to_x + to_dir_vec.x, to_y + to_dir_vec.y),
140                                (ctx.to_screen)(to_x, to_y),
141                            ],
142                            closed: false,
143                            fill: Color32::TRANSPARENT,
144                            stroke: Stroke { width: 3., color }.into(),
145                        });
146                        ctx.painter.add(shape);
147
148                        ctx.painter.text(
149                            (ctx.to_screen)(to_x, to_y),
150                            match to_dir {
151                                Direction::North => Align2([Align::Center, Align::TOP]),
152                                Direction::East => Align2([Align::LEFT, Align::Center]),
153                                Direction::South => Align2([Align::Center, Align::BOTTOM]),
154                                Direction::West => Align2([Align::RIGHT, Align::Center]),
155                            },
156                            text,
157                            FontId::monospace(15.),
158                            color,
159                        );
160                    }
161                }
162                Graphic::Text {
163                    pos: (pos, dir),
164                    text,
165                } => {
166                    let to_x =
167                        viewport.pixel_from_time(&pos.x, ctx.cfg.canvas_size.x, &num_timestamps);
168                    let to_y = self.get_item_y(&pos.y);
169                    if let Some(to_y) = to_y {
170                        ctx.painter.text(
171                            (ctx.to_screen)(to_x, to_y),
172                            match dir {
173                                Direction::North => Align2([Align::Center, Align::TOP]),
174                                Direction::East => Align2([Align::LEFT, Align::Center]),
175                                Direction::South => Align2([Align::Center, Align::BOTTOM]),
176                                Direction::West => Align2([Align::RIGHT, Align::Center]),
177                            },
178                            text,
179                            FontId::monospace(15.),
180                            color,
181                        );
182                    }
183                }
184                Graphic::Rectangle {
185                    from: from_point,
186                    to: to_point,
187                } => {
188                    let from_x = viewport.pixel_from_time(
189                        &from_point.x,
190                        ctx.cfg.canvas_size.x,
191                        &num_timestamps,
192                    );
193                    let from_y = self.get_item_y(&from_point.y);
194                    let to_x = viewport.pixel_from_time(
195                        &from_point.x,
196                        ctx.cfg.canvas_size.x,
197                        &num_timestamps,
198                    );
199                    let to_y = self.get_item_y(&to_point.y);
200
201                    if let (Some(from_y), Some(to_y)) = (from_y, to_y) {
202                        let start_pos = (ctx.to_screen)(from_x, from_y);
203                        let end_pos = (ctx.to_screen)(to_x, to_y);
204                        let temp_rect = emath::Rect::from_two_pos(start_pos, end_pos);
205                        let stroke: Stroke = Stroke { width: 3., color };
206
207                        ctx.painter
208                            .rect_stroke(temp_rect, 0.0, stroke, egui::StrokeKind::Middle);
209                    }
210                }
211            }
212        }
213    }
214}