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 range = self.time_range();
64 draw_vertical_line_at_time(cursor_time, ctx, &theme.cursor, viewport, range);
65 }
66 }
67
68 pub fn draw_markers(&self, theme: &SurferTheme, ctx: &mut DrawingContext, viewport: &Viewport) {
69 let range = self.time_range();
70 for (idx, marker_time) in &self.markers {
71 let color = self.get_marker_color(*idx, theme);
72 let stroke = Stroke {
73 color,
74 width: theme.cursor.width,
75 };
76 draw_vertical_line_at_time(marker_time, ctx, stroke, viewport, range);
77 }
78 }
79
80 #[must_use]
81 pub fn can_add_marker(&self) -> bool {
82 self.markers.len() < MAX_MARKERS
83 }
84
85 pub fn add_marker(
86 &mut self,
87 location: &BigInt,
88 name: Option<String>,
89 move_focus: bool,
90 ) -> Option<DisplayedItemRef> {
91 if !self.can_add_marker() {
92 return None;
93 }
94
95 let Some(idx) = (0..=MAX_MARKER_INDEX).find(|idx| !self.markers.contains_key(idx)) else {
96 return None;
99 };
100
101 let item_ref = self.insert_item(
102 DisplayedItem::Marker(DisplayedMarker {
103 color: None,
104 background_color: None,
105 name,
106 idx,
107 }),
108 None,
109 move_focus,
110 );
111 self.markers.insert(idx, location.clone());
112
113 Some(item_ref)
114 }
115
116 pub fn remove_marker(&mut self, idx: u8) {
117 if let Some(&marker_item_ref) =
118 self.displayed_items
119 .iter()
120 .find_map(|(id, item)| match item {
121 DisplayedItem::Marker(marker) if marker.idx == idx => Some(id),
122 _ => None,
123 })
124 {
125 self.remove_displayed_item(marker_item_ref);
126 }
127 }
128
129 pub fn set_marker_position(&mut self, idx: u8, location: &BigInt) {
133 if !self.markers.contains_key(&idx) {
134 self.insert_item(
135 DisplayedItem::Marker(DisplayedMarker {
136 color: None,
137 background_color: None,
138 name: None,
139 idx,
140 }),
141 None,
142 true,
143 );
144 }
145 self.markers.insert(idx, location.clone());
146 }
147
148 pub fn move_marker_to_cursor(&mut self, idx: u8) {
149 if let Some(location) = self.cursor.clone() {
150 self.set_marker_position(idx, &location);
151 }
152 }
153
154 fn draw_text_with_background(
157 ctx: &mut DrawingContext,
158 x: f32,
159 text: &str,
160 background_color: Color32,
161 foreground_color: Color32,
162 padding: f32,
163 ) {
164 let y = ctx.cfg.canvas_size.y * 0.5;
165 let text_size = ctx.cfg.text_size;
166 let rect = ctx.painter.text(
168 (ctx.to_screen)(x, y),
169 Align2::CENTER_CENTER,
170 text,
171 FontId::proportional(text_size),
172 foreground_color,
173 );
174
175 let min = Pos2::new(rect.min.x - padding, rect.min.y - padding);
177 let max = Pos2::new(rect.max.x + padding, rect.max.y + padding);
178
179 ctx.painter
180 .rect_filled(Rect { min, max }, CornerRadius::ZERO, background_color);
181
182 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
192 pub fn draw_marker_number_boxes(
193 &self,
194 ctx: &mut DrawingContext,
195 theme: &SurferTheme,
196 viewport: &Viewport,
197 ) {
198 for displayed_item in self
199 .items_tree
200 .iter_visible()
201 .map(|node| self.displayed_items.get(&node.item_ref))
202 .filter_map(|item| match item {
203 Some(DisplayedItem::Marker(marker)) => Some(marker),
204 _ => None,
205 })
206 {
207 let item = DisplayedItem::Marker(displayed_item.clone());
208 let background_color = get_marker_background_color(&item, theme);
209
210 let x =
211 self.numbered_marker_location(displayed_item.idx, viewport, ctx.cfg.canvas_size.x);
212 let idx_string = displayed_item.idx.to_string();
213
214 Self::draw_text_with_background(
215 ctx,
216 x,
217 &idx_string,
218 background_color,
219 theme.foreground,
220 2.0,
221 );
222 }
223 }
224}
225
226impl SystemState {
227 pub fn draw_marker_window(&self, waves: &WaveData, ctx: &Context, msgs: &mut Vec<Message>) {
228 let mut open = true;
229
230 let markers: Vec<(u8, &BigInt, WidgetText)> = waves
232 .cursor
233 .as_ref()
234 .into_iter()
235 .map(|cursor| {
236 (
237 CURSOR_MARKER_IDX,
238 cursor,
239 WidgetText::RichText(RichText::new("Primary").into()),
240 )
241 })
242 .chain(
243 waves
244 .items_tree
245 .iter()
246 .filter_map(|node| waves.displayed_items.get(&node.item_ref))
247 .filter_map(|displayed_item| match displayed_item {
248 DisplayedItem::Marker(marker) => {
249 let text_color = self.get_item_text_color(displayed_item);
250 Some((
251 marker.idx,
252 waves.numbered_marker_time(marker.idx),
253 marker.marker_text(text_color),
254 ))
255 }
256 _ => None,
257 })
258 .sorted_by(|a, b| Ord::cmp(&a.0, &b.0)),
259 )
260 .collect();
261
262 Window::new("Markers")
263 .collapsible(true)
264 .resizable(true)
265 .open(&mut open)
266 .show(ctx, |ui| {
267 ui.vertical_centered(|ui| {
268 let row_height = ui.text_style_height(&egui::TextStyle::Body);
270 TableBuilder::new(ui)
271 .striped(true)
272 .cell_layout(egui::Layout::right_to_left(emath::Align::TOP))
273 .columns(Column::auto().resizable(true), markers.len() + 1)
274 .auto_shrink(emath::Vec2b::new(false, true))
275 .header(row_height, |mut header| {
276 header.col(|ui| {
277 ui.label("");
278 });
279 for (marker_idx, _, widget_text) in &markers {
280 header.col(|ui| {
281 if ui.label(widget_text.clone()).clicked() {
282 msgs.push(marker_click_message(
283 *marker_idx,
284 waves.cursor.as_ref(),
285 ));
286 }
287 });
288 }
289 })
290 .body(|body| {
291 let time_formatter = TimeFormatter::new(
292 &waves.inner.metadata().timescale,
293 &self.user.wanted_timeunit,
294 &self.get_time_format(),
295 );
296 let numbber_of_markers = markers.len();
297 body.rows(row_height, numbber_of_markers, |mut row| {
298 let row_idx = row.index();
299 let (marker_idx, row_marker_time, row_widget_text) =
300 &markers[row_idx];
301 row.col(|ui| {
302 if ui.label(row_widget_text.clone()).clicked() {
303 msgs.push(marker_click_message(
304 *marker_idx,
305 waves.cursor.as_ref(),
306 ));
307 }
308 });
309 for (_, col_marker_time, _) in &markers {
310 let diff = time_formatter
311 .format(&(*row_marker_time - *col_marker_time));
312 row.col(|ui| {
313 ui.label(diff);
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 viewport: &Viewport,
334 row_offset: f32,
335 ) {
336 let horizontal_padding = self.user.config.layout.waveforms_gap;
337
338 let time_formatter = TimeFormatter::new(
339 &waves.inner.metadata().timescale,
340 &self.user.wanted_timeunit,
341 &self.get_time_format(),
342 );
343 let visible_top = -row_offset;
344 let visible_bottom = ctx.cfg.canvas_size.y - row_offset;
345 for drawing_info in waves
346 .visible_drawing_infos(visible_top, visible_bottom)
347 .filter_map(|item| match item {
348 ItemDrawingInfo::Marker(marker) => Some(marker),
349 _ => None,
350 })
351 {
352 let Some(item) = waves
353 .items_tree
354 .get_visible(drawing_info.vidx)
355 .and_then(|node| waves.displayed_items.get(&node.item_ref))
356 else {
357 continue;
358 };
359
360 let row_top = drawing_info.top + row_offset;
361 let row_bottom = drawing_info.bottom + row_offset;
362
363 let background_color = get_marker_background_color(item, &self.user.config.theme);
364
365 let x =
366 waves.numbered_marker_location(drawing_info.idx, viewport, ctx.cfg.canvas_size.x);
367
368 let time = time_formatter.format(
370 waves
371 .markers
372 .get(&drawing_info.idx)
373 .unwrap_or(&BigInt::zero()),
374 );
375
376 let text_color = self.user.config.theme.get_best_text_color(background_color);
377
378 let galley = ctx.painter.layout_no_wrap(
380 time,
381 FontId::proportional(ctx.cfg.text_size),
382 text_color,
383 );
384 let offset_width = galley.rect.width() * 0.5 + horizontal_padding;
385
386 let min = (ctx.to_screen)(x - offset_width, row_top);
388 let max = (ctx.to_screen)(x + offset_width, row_bottom);
389
390 ctx.painter
391 .rect_filled(Rect { min, max }, CornerRadius::ZERO, background_color);
392
393 ctx.painter.galley(
395 (ctx.to_screen)(
396 x - galley.rect.width() * 0.5,
397 (row_top + row_bottom - galley.rect.height()) * 0.5,
398 ),
399 galley,
400 text_color,
401 );
402 }
403 }
404}
405
406fn get_marker_background_color(item: &DisplayedItem, theme: &SurferTheme) -> Color32 {
408 item.color()
409 .and_then(|color| theme.get_color(color))
410 .unwrap_or(theme.cursor.color)
411}
412
413fn marker_click_message(marker_idx: u8, cursor: Option<&BigInt>) -> Message {
415 if marker_idx < CURSOR_MARKER_IDX {
416 Message::GoToMarkerPosition(marker_idx, 0)
417 } else {
418 Message::GoToTime(cursor.cloned(), 0)
419 }
420}