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