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