libsurfer/
graphics.rs

1use eframe::{
2    emath::{Align, Align2},
3    epaint::{Color32, CubicBezierShape, FontId, Shape, Stroke, Vec2},
4};
5use num::BigInt;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    config::SurferTheme, displayed_item::DisplayedItemRef, view::DrawingContext,
10    viewport::Viewport, wave_data::WaveData,
11};
12
13#[derive(Serialize, Deserialize, Debug)]
14pub enum Direction {
15    North,
16    East,
17    South,
18    West,
19}
20
21impl Direction {
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)]
33pub enum Anchor {
34    Top,
35    Center,
36    Bottom,
37}
38
39#[derive(Serialize, Deserialize, Debug)]
40pub struct GraphicsY {
41    pub item: DisplayedItemRef,
42    pub anchor: Anchor,
43}
44
45/// A point used to place graphics.
46#[derive(Serialize, Deserialize, Debug)]
47pub struct GrPoint {
48    /// Timestamp at which to place the graphic
49    pub x: BigInt,
50    pub y: GraphicsY,
51}
52
53#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord, Hash, Debug)]
54pub struct GraphicId(pub usize);
55
56#[derive(Serialize, Deserialize, Debug)]
57pub enum Graphic {
58    TextArrow {
59        from: (GrPoint, Direction),
60        to: (GrPoint, Direction),
61        text: String,
62    },
63    Text {
64        pos: (GrPoint, Direction),
65        text: String,
66    },
67}
68
69impl WaveData {
70    // FIXME: This function should probably not be here, we should instead update ItemDrawingInfo to
71    // have this info
72    fn get_item_y(&self, y: &GraphicsY) -> Option<f32> {
73        self.items_tree
74            .iter_visible()
75            .zip(&self.drawing_infos)
76            .find(|(node, _info)| node.item_ref == y.item)
77            .map(|(_, info)| match y.anchor {
78                Anchor::Top => info.top(),
79                Anchor::Center => info.top() + (info.bottom() - info.top()) / 2.,
80                Anchor::Bottom => info.bottom(),
81            })
82            .map(|point| point - self.top_item_draw_offset)
83    }
84
85    pub(crate) fn draw_graphics(
86        &self,
87        ctx: &mut DrawingContext,
88        size: Vec2,
89        viewport: &Viewport,
90        theme: &SurferTheme,
91    ) {
92        let color = theme.variable_dontcare;
93        let num_timestamps = self.num_timestamps().unwrap_or(1.into());
94        for g in self.graphics.values() {
95            match g {
96                Graphic::TextArrow {
97                    from: (from_point, from_dir),
98                    to: (to_point, to_dir),
99                    text,
100                } => {
101                    let from_x = viewport.pixel_from_time(&from_point.x, size.x, &num_timestamps);
102                    let from_y = self.get_item_y(&from_point.y);
103
104                    let to_x = viewport.pixel_from_time(&to_point.x, size.x, &num_timestamps);
105                    let to_y = self.get_item_y(&to_point.y);
106
107                    if let (Some(from_y), Some(to_y)) = (from_y, to_y) {
108                        let from_dir = from_dir.as_vector() * 30.;
109                        let to_dir_vec = to_dir.as_vector() * 30.;
110                        let shape = Shape::CubicBezier(CubicBezierShape {
111                            points: [
112                                (ctx.to_screen)(from_x, from_y),
113                                (ctx.to_screen)(from_x + from_dir.x, from_y + from_dir.y),
114                                (ctx.to_screen)(to_x + to_dir_vec.x, to_y + to_dir_vec.y),
115                                (ctx.to_screen)(to_x, to_y),
116                            ],
117                            closed: false,
118                            fill: Color32::TRANSPARENT,
119                            stroke: Stroke { width: 3., color }.into(),
120                        });
121                        ctx.painter.add(shape);
122
123                        ctx.painter.text(
124                            (ctx.to_screen)(to_x, to_y),
125                            match to_dir {
126                                Direction::North => Align2([Align::Center, Align::TOP]),
127                                Direction::East => Align2([Align::LEFT, Align::Center]),
128                                Direction::South => Align2([Align::Center, Align::BOTTOM]),
129                                Direction::West => Align2([Align::RIGHT, Align::Center]),
130                            },
131                            text,
132                            FontId::monospace(15.),
133                            color,
134                        );
135                    }
136                }
137                Graphic::Text {
138                    pos: (pos, dir),
139                    text,
140                } => {
141                    let to_x = viewport.pixel_from_time(&pos.x, size.x, &num_timestamps);
142                    let to_y = self.get_item_y(&pos.y);
143                    if let Some(to_y) = to_y {
144                        ctx.painter.text(
145                            (ctx.to_screen)(to_x, to_y),
146                            match dir {
147                                Direction::North => Align2([Align::Center, Align::TOP]),
148                                Direction::East => Align2([Align::LEFT, Align::Center]),
149                                Direction::South => Align2([Align::Center, Align::BOTTOM]),
150                                Direction::West => Align2([Align::RIGHT, Align::Center]),
151                            },
152                            text,
153                            FontId::monospace(15.),
154                            color,
155                        );
156                    }
157                }
158            }
159        }
160    }
161}