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;
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_size.x, &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_size.x, &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 fn draw_text_with_background(
143 ctx: &mut DrawingContext,
144 x: f32,
145 text: &str,
146 background_color: Color32,
147 foreground_color: Color32,
148 padding: f32,
149 ) {
150 let y = ctx.cfg.canvas_size.y * 0.5;
151 let text_size = ctx.cfg.text_size;
152 let rect = ctx.painter.text(
154 (ctx.to_screen)(x, y),
155 Align2::CENTER_CENTER,
156 text,
157 FontId::proportional(text_size),
158 foreground_color,
159 );
160
161 let min = Pos2::new(rect.min.x - padding, rect.min.y - padding);
163 let max = Pos2::new(rect.max.x + padding, rect.max.y + padding);
164
165 ctx.painter
166 .rect_filled(Rect { min, max }, CornerRadius::ZERO, background_color);
167
168 ctx.painter.text(
170 (ctx.to_screen)(x, y),
171 Align2::CENTER_CENTER,
172 text,
173 FontId::proportional(text_size),
174 foreground_color,
175 );
176 }
177
178 pub fn draw_marker_number_boxes(
179 &self,
180 ctx: &mut DrawingContext,
181 theme: &SurferTheme,
182 viewport: &Viewport,
183 ) {
184 for displayed_item in self
185 .items_tree
186 .iter_visible()
187 .map(|node| self.displayed_items.get(&node.item_ref))
188 .filter_map(|item| match item {
189 Some(DisplayedItem::Marker(marker)) => Some(marker),
190 _ => None,
191 })
192 {
193 let item = DisplayedItem::Marker(displayed_item.clone());
194 let background_color = get_marker_background_color(&item, theme);
195
196 let x =
197 self.numbered_marker_location(displayed_item.idx, viewport, ctx.cfg.canvas_size.x);
198 let idx_string = displayed_item.idx.to_string();
199
200 Self::draw_text_with_background(
201 ctx,
202 x,
203 &idx_string,
204 background_color,
205 theme.foreground,
206 2.0,
207 );
208 }
209 }
210}
211
212impl SystemState {
213 pub fn draw_marker_window(&self, waves: &WaveData, ctx: &Context, msgs: &mut Vec<Message>) {
214 let mut open = true;
215
216 let markers: Vec<(u8, &BigInt, WidgetText)> = waves
218 .cursor
219 .as_ref()
220 .into_iter()
221 .map(|cursor| {
222 (
223 CURSOR_MARKER_IDX,
224 cursor,
225 WidgetText::RichText(RichText::new("Primary").into()),
226 )
227 })
228 .chain(
229 waves
230 .items_tree
231 .iter()
232 .filter_map(|node| waves.displayed_items.get(&node.item_ref))
233 .filter_map(|displayed_item| match displayed_item {
234 DisplayedItem::Marker(marker) => {
235 let text_color = self.get_item_text_color(displayed_item);
236 Some((
237 marker.idx,
238 waves.numbered_marker_time(marker.idx),
239 marker.marker_text(text_color),
240 ))
241 }
242 _ => None,
243 })
244 .sorted_by(|a, b| Ord::cmp(&a.0, &b.0)),
245 )
246 .collect();
247
248 Window::new("Markers")
249 .collapsible(true)
250 .resizable(true)
251 .open(&mut open)
252 .show(ctx, |ui| {
253 ui.vertical_centered(|ui| {
254 let row_height = ui.text_style_height(&egui::TextStyle::Body);
256 TableBuilder::new(ui)
257 .striped(true)
258 .cell_layout(egui::Layout::right_to_left(emath::Align::TOP))
259 .columns(Column::auto().resizable(true), markers.len() + 1)
260 .auto_shrink(emath::Vec2b::new(false, true))
261 .header(row_height, |mut header| {
262 header.col(|ui| {
263 ui.label("");
264 });
265 for (marker_idx, _, widget_text) in &markers {
266 header.col(|ui| {
267 if ui.label(widget_text.clone()).clicked() {
268 msgs.push(marker_click_message(
269 *marker_idx,
270 waves.cursor.as_ref(),
271 ));
272 }
273 });
274 }
275 })
276 .body(|mut body| {
277 let time_formatter = TimeFormatter::new(
278 &waves.inner.metadata().timescale,
279 &self.user.wanted_timeunit,
280 &self.get_time_format(),
281 );
282 for (marker_idx, row_marker_time, row_widget_text) in &markers {
283 body.row(row_height, |mut row| {
284 row.col(|ui| {
285 if ui.label(row_widget_text.clone()).clicked() {
286 msgs.push(marker_click_message(
287 *marker_idx,
288 waves.cursor.as_ref(),
289 ));
290 }
291 });
292 for (_, col_marker_time, _) in &markers {
293 let diff = time_formatter
294 .format(&(*row_marker_time - *col_marker_time));
295 row.col(|ui| {
296 ui.label(diff);
297 });
298 }
299 });
300 }
301 });
302 ui.add_space(15.);
303 if ui.button("Close").clicked() {
304 msgs.push(Message::SetCursorWindowVisible(false));
305 }
306 });
307 });
308 if !open {
309 msgs.push(Message::SetCursorWindowVisible(false));
310 }
311 }
312
313 pub fn draw_marker_boxes(
314 &self,
315 waves: &WaveData,
316 ctx: &mut DrawingContext,
317 viewport: &Viewport,
318 y_zero: f32,
319 ) {
320 let horizontal_padding = self.user.config.layout.waveforms_gap;
321
322 let time_formatter = TimeFormatter::new(
323 &waves.inner.metadata().timescale,
324 &self.user.wanted_timeunit,
325 &self.get_time_format(),
326 );
327 for drawing_info in waves.drawing_infos.iter().filter_map(|item| match item {
328 ItemDrawingInfo::Marker(marker) => Some(marker),
329 _ => None,
330 }) {
331 let Some(item) = waves
332 .items_tree
333 .get_visible(drawing_info.vidx)
334 .and_then(|node| waves.displayed_items.get(&node.item_ref))
335 else {
336 continue;
337 };
338
339 let row_top = drawing_info.top - y_zero;
343 let row_bottom = drawing_info.bottom - y_zero;
344
345 let background_color = get_marker_background_color(item, &self.user.config.theme);
346
347 let x =
348 waves.numbered_marker_location(drawing_info.idx, viewport, ctx.cfg.canvas_size.x);
349
350 let time = time_formatter.format(
352 waves
353 .markers
354 .get(&drawing_info.idx)
355 .unwrap_or(&BigInt::zero()),
356 );
357
358 let text_color = self.user.config.theme.get_best_text_color(background_color);
359
360 let galley = ctx.painter.layout_no_wrap(
362 time,
363 FontId::proportional(ctx.cfg.text_size),
364 text_color,
365 );
366 let offset_width = galley.rect.width() * 0.5 + horizontal_padding;
367
368 let min = (ctx.to_screen)(x - offset_width, row_top);
370 let max = (ctx.to_screen)(x + offset_width, row_bottom);
371
372 ctx.painter
373 .rect_filled(Rect { min, max }, CornerRadius::ZERO, background_color);
374
375 ctx.painter.galley(
377 (ctx.to_screen)(
378 x - galley.rect.width() * 0.5,
379 (row_top + row_bottom - galley.rect.height()) * 0.5,
380 ),
381 galley,
382 text_color,
383 );
384 }
385 }
386}
387
388fn get_marker_background_color(item: &DisplayedItem, theme: &SurferTheme) -> Color32 {
390 item.color()
391 .and_then(|color| theme.get_color(color))
392 .unwrap_or(theme.cursor.color)
393}
394
395fn marker_click_message(marker_idx: u8, cursor: Option<&BigInt>) -> Message {
397 if marker_idx < CURSOR_MARKER_IDX {
398 Message::GoToMarkerPosition(marker_idx, 0)
399 } else {
400 Message::GoToTime(cursor.cloned(), 0)
401 }
402}