1use ecolor::Color32;
2use egui::{Context, RichText, WidgetText, Window};
3use egui_extras::{Column, TableBuilder};
4use emath::{Align2, Pos2, Rect};
5use epaint::{CornerRadius, FontId, Stroke};
6use itertools::Itertools;
7use num::BigInt;
8
9use crate::SystemState;
10use crate::drawing_canvas::draw_vertical_line;
11use crate::{
12 config::SurferTheme,
13 displayed_item::{DisplayedItem, DisplayedItemRef, DisplayedMarker},
14 message::Message,
15 time::TimeFormatter,
16 view::{DrawingContext, ItemDrawingInfo},
17 viewport::Viewport,
18 wave_data::WaveData,
19};
20
21pub const DEFAULT_MARKER_NAME: &str = "Marker";
22const MAX_MARKERS: usize = 255;
23const MAX_MARKER_INDEX: u8 = 254;
24const CURSOR_MARKER_IDX: u8 = 255;
25
26impl WaveData {
27 fn get_marker_color(&self, idx: u8, theme: &SurferTheme) -> Color32 {
29 self.items_tree
30 .iter()
31 .find_map(|node| {
32 if let Some(DisplayedItem::Marker(marker)) =
33 self.displayed_items.get(&node.item_ref)
34 && marker.idx == idx
35 {
36 return marker
37 .color
38 .as_ref()
39 .and_then(|color| theme.get_color(color));
40 }
41 None
42 })
43 .unwrap_or(theme.cursor.color)
44 }
45
46 pub fn draw_cursor(&self, theme: &SurferTheme, ctx: &mut DrawingContext, viewport: &Viewport) {
47 if let Some(marker) = &self.cursor {
48 let num_timestamps = self.safe_num_timestamps();
49 let x = viewport.pixel_from_time(marker, ctx.cfg.canvas_width, &num_timestamps);
50 draw_vertical_line(x, ctx, &theme.cursor);
51 }
52 }
53
54 pub fn draw_markers(&self, theme: &SurferTheme, ctx: &mut DrawingContext, viewport: &Viewport) {
55 let num_timestamps = self.safe_num_timestamps();
56 for (idx, marker) in &self.markers {
57 let color = self.get_marker_color(*idx, theme);
58 let stroke = Stroke {
59 color,
60 width: theme.cursor.width,
61 };
62 let x = viewport.pixel_from_time(marker, ctx.cfg.canvas_width, &num_timestamps);
63 draw_vertical_line(x, ctx, stroke);
64 }
65 }
66
67 #[must_use]
68 pub fn can_add_marker(&self) -> bool {
69 self.markers.len() < MAX_MARKERS
70 }
71
72 pub fn add_marker(
73 &mut self,
74 location: &BigInt,
75 name: Option<String>,
76 move_focus: bool,
77 ) -> Option<DisplayedItemRef> {
78 if !self.can_add_marker() {
79 return None;
80 }
81
82 let Some(idx) = (0..=MAX_MARKER_INDEX).find(|idx| !self.markers.contains_key(idx)) else {
83 return None;
86 };
87
88 let item_ref = self.insert_item(
89 DisplayedItem::Marker(DisplayedMarker {
90 color: None,
91 background_color: None,
92 name,
93 idx,
94 }),
95 None,
96 move_focus,
97 );
98 self.markers.insert(idx, location.clone());
99
100 Some(item_ref)
101 }
102
103 pub fn remove_marker(&mut self, idx: u8) {
104 if let Some(&marker_item_ref) =
105 self.displayed_items
106 .iter()
107 .find_map(|(id, item)| match item {
108 DisplayedItem::Marker(marker) if marker.idx == idx => Some(id),
109 _ => None,
110 })
111 {
112 self.remove_displayed_item(marker_item_ref);
113 }
114 }
115
116 pub fn set_marker_position(&mut self, idx: u8, location: &BigInt) {
119 if !self.markers.contains_key(&idx) {
120 self.insert_item(
121 DisplayedItem::Marker(DisplayedMarker {
122 color: None,
123 background_color: None,
124 name: None,
125 idx,
126 }),
127 None,
128 true,
129 );
130 }
131 self.markers.insert(idx, location.clone());
132 }
133
134 pub fn move_marker_to_cursor(&mut self, idx: u8) {
135 if let Some(location) = self.cursor.clone() {
136 self.set_marker_position(idx, &location);
137 }
138 }
139
140 #[allow(clippy::too_many_arguments)]
143 fn draw_text_with_background(
144 ctx: &mut DrawingContext,
145 x: f32,
146 y: f32,
147 text: &str,
148 text_size: f32,
149 background_color: Color32,
150 foreground_color: Color32,
151 padding: f32,
152 ) {
153 let rect = ctx.painter.text(
155 (ctx.to_screen)(x, y),
156 Align2::CENTER_CENTER,
157 text,
158 FontId::proportional(text_size),
159 foreground_color,
160 );
161
162 let min = Pos2::new(rect.min.x - padding, rect.min.y - padding);
164 let max = Pos2::new(rect.max.x + padding, rect.max.y + padding);
165
166 ctx.painter
167 .rect_filled(Rect { min, max }, CornerRadius::ZERO, background_color);
168
169 ctx.painter.text(
171 (ctx.to_screen)(x, y),
172 Align2::CENTER_CENTER,
173 text,
174 FontId::proportional(text_size),
175 foreground_color,
176 );
177 }
178
179 pub fn draw_marker_number_boxes(
180 &self,
181 ctx: &mut DrawingContext,
182 theme: &SurferTheme,
183 viewport: &Viewport,
184 ) {
185 let text_size = ctx.cfg.text_size;
186
187 for displayed_item in self
188 .items_tree
189 .iter_visible()
190 .map(|node| self.displayed_items.get(&node.item_ref))
191 .filter_map(|item| match item {
192 Some(DisplayedItem::Marker(marker)) => Some(marker),
193 _ => None,
194 })
195 {
196 let item = DisplayedItem::Marker(displayed_item.clone());
197 let background_color = get_marker_background_color(&item, theme);
198
199 let x =
200 self.numbered_marker_location(displayed_item.idx, viewport, ctx.cfg.canvas_width);
201 let idx_string = displayed_item.idx.to_string();
202
203 Self::draw_text_with_background(
204 ctx,
205 x,
206 ctx.cfg.canvas_height * 0.5,
207 &idx_string,
208 text_size,
209 background_color,
210 theme.foreground,
211 2.0,
212 );
213 }
214 }
215}
216
217impl SystemState {
218 pub fn draw_marker_window(&self, waves: &WaveData, ctx: &Context, msgs: &mut Vec<Message>) {
219 let mut open = true;
220
221 let markers: Vec<(u8, &BigInt, WidgetText)> = waves
223 .cursor
224 .as_ref()
225 .into_iter()
226 .map(|cursor| {
227 (
228 CURSOR_MARKER_IDX,
229 cursor,
230 WidgetText::RichText(RichText::new("Primary").into()),
231 )
232 })
233 .chain(
234 waves
235 .items_tree
236 .iter()
237 .filter_map(|node| waves.displayed_items.get(&node.item_ref))
238 .filter_map(|displayed_item| match displayed_item {
239 DisplayedItem::Marker(marker) => {
240 let text_color = self.get_item_text_color(displayed_item);
241 Some((
242 marker.idx,
243 waves.numbered_marker_time(marker.idx),
244 marker.marker_text(text_color),
245 ))
246 }
247 _ => None,
248 })
249 .sorted_by(|a, b| Ord::cmp(&a.0, &b.0)),
250 )
251 .collect();
252
253 Window::new("Markers")
254 .collapsible(true)
255 .resizable(true)
256 .open(&mut open)
257 .show(ctx, |ui| {
258 ui.vertical_centered(|ui| {
259 let row_height = ui.text_style_height(&egui::TextStyle::Body);
261 TableBuilder::new(ui)
262 .striped(true)
263 .cell_layout(egui::Layout::right_to_left(emath::Align::TOP))
264 .columns(Column::auto().resizable(true), markers.len() + 1)
265 .auto_shrink(emath::Vec2b::new(false, true))
266 .header(row_height, |mut header| {
267 header.col(|ui| {
268 ui.label("");
269 });
270 for (marker_idx, _, widget_text) in &markers {
271 header.col(|ui| {
272 if ui.label(widget_text.clone()).clicked() {
273 msgs.push(marker_click_message(
274 *marker_idx,
275 waves.cursor.as_ref(),
276 ));
277 }
278 });
279 }
280 })
281 .body(|mut body| {
282 let time_formatter = TimeFormatter::new(
283 &waves.inner.metadata().timescale,
284 &self.user.wanted_timeunit,
285 &self.get_time_format(),
286 );
287 for (marker_idx, row_marker_time, row_widget_text) in &markers {
288 body.row(row_height, |mut row| {
289 row.col(|ui| {
290 if ui.label(row_widget_text.clone()).clicked() {
291 msgs.push(marker_click_message(
292 *marker_idx,
293 waves.cursor.as_ref(),
294 ));
295 }
296 });
297 for (_, col_marker_time, _) in &markers {
298 let diff = time_formatter
299 .format(&(*row_marker_time - *col_marker_time));
300 row.col(|ui| {
301 ui.label(diff);
302 });
303 }
304 });
305 }
306 });
307 ui.add_space(15.);
308 if ui.button("Close").clicked() {
309 msgs.push(Message::SetCursorWindowVisible(false));
310 }
311 });
312 });
313 if !open {
314 msgs.push(Message::SetCursorWindowVisible(false));
315 }
316 }
317
318 pub fn draw_marker_boxes(
319 &self,
320 waves: &WaveData,
321 ctx: &mut DrawingContext,
322 gap: f32,
323 viewport: &Viewport,
324 y_zero: f32,
325 ) {
326 let text_size = ctx.cfg.text_size;
327
328 let time_formatter = TimeFormatter::new(
329 &waves.inner.metadata().timescale,
330 &self.user.wanted_timeunit,
331 &self.get_time_format(),
332 );
333 for drawing_info in waves.drawing_infos.iter().filter_map(|item| match item {
334 ItemDrawingInfo::Marker(marker) => Some(marker),
335 _ => None,
336 }) {
337 let Some(item) = waves
338 .items_tree
339 .get_visible(drawing_info.vidx)
340 .and_then(|node| waves.displayed_items.get(&node.item_ref))
341 else {
342 return;
343 };
344
345 let y_offset = drawing_info.top - y_zero;
349 let y_bottom = drawing_info.bottom - y_zero;
350
351 let background_color = get_marker_background_color(item, &self.user.config.theme);
352
353 let x =
354 waves.numbered_marker_location(drawing_info.idx, viewport, ctx.cfg.canvas_width);
355
356 let time = time_formatter.format(
358 waves
359 .markers
360 .get(&drawing_info.idx)
361 .unwrap_or(&BigInt::from(0)),
362 );
363
364 let text_color = self.user.config.theme.get_best_text_color(background_color);
365
366 let galley =
368 ctx.painter
369 .layout_no_wrap(time, FontId::proportional(text_size), text_color);
370 let offset_width = galley.rect.width() * 0.5 + 2. * gap;
371
372 let min = (ctx.to_screen)(x - offset_width, y_offset - gap);
374 let max = (ctx.to_screen)(x + offset_width, y_bottom + gap);
375
376 ctx.painter
377 .rect_filled(Rect { min, max }, CornerRadius::ZERO, background_color);
378
379 ctx.painter.galley(
381 (ctx.to_screen)(
382 x - galley.rect.width() * 0.5,
383 (y_offset + y_bottom - galley.rect.height()) * 0.5,
384 ),
385 galley,
386 text_color,
387 );
388 }
389 }
390}
391
392fn get_marker_background_color(item: &DisplayedItem, theme: &SurferTheme) -> Color32 {
394 item.color()
395 .and_then(|color| theme.get_color(color))
396 .unwrap_or(theme.cursor.color)
397}
398
399fn marker_click_message(marker_idx: u8, cursor: Option<&BigInt>) -> Message {
401 if marker_idx < CURSOR_MARKER_IDX {
402 Message::GoToMarkerPosition(marker_idx, 0)
403 } else {
404 Message::GoToTime(cursor.cloned(), 0)
405 }
406}