1use crate::{Message, annotation::Annotatable, time::TimeFormatter, wave_data::WaveData};
2use egui::{Align, Color32, Key, Layout, Ui};
3use egui_remixicon::icons;
4use tracing::warn;
5
6#[derive(Clone, Default)]
7pub struct AnnotationList {}
8
9const DEFAULT_GROUP_NAME: &str = "Ungrouped";
10const TIME_FONT_SIZE: f32 = 11.;
11const DEFAULT_SPACE: f32 = 4.;
12const WIDTH_CONSTRAINT: f32 = 30.;
13
14impl AnnotationList {}
15
16#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
17pub struct AnnotationGroup {
18 pub name: String,
19 pub cycle_counter: usize,
20 pub annotations: Vec<egui::Id>,
21}
22
23impl AnnotationList {}
24
25impl WaveData {
26 pub fn draw_annotation_list(
27 &self,
28 ui: &mut Ui,
29 msgs: &mut Vec<Message>,
30 time_formatter: &TimeFormatter,
31 annotation_groups: &mut [AnnotationGroup],
32 ) {
33 ui.style_mut()
34 .visuals
35 .widgets
36 .noninteractive
37 .bg_stroke
38 .width = 0.5;
39
40 ui.horizontal(|ui| {
41 ui.allocate_space(egui::vec2(ui.available_width() - WIDTH_CONSTRAINT, 0.0));
42 if ui.button(icons::CLOSE_LARGE_LINE).clicked() {
43 msgs.push(Message::ToggleAnnotationlistVisibility());
44 }
45 });
46
47 ui.vertical_centered(|ui| {
48 ui.heading("Annotation List");
49 if self.annotations.is_empty() {
50 ui.label("Your annotations will be displayed here.");
51 }
52 });
53
54 ui.add_space(DEFAULT_SPACE * 2.);
55 ui.separator();
56
57 ui.horizontal(|ui| {
59 ui.add_space(DEFAULT_SPACE * 2.);
60 ui.label(egui::RichText::new("Manage Groups").small().strong());
61 });
62 ui.horizontal(|ui| {
63 ui.add_space(DEFAULT_SPACE * 2.);
64 let input_id = ui.make_persistent_id("group_input_buffer");
65 let mut buffer = ui.data_mut(|d| d.get_temp::<String>(input_id).unwrap_or_default());
66
67 let text_edit_res = ui.add(
68 egui::TextEdit::singleline(&mut buffer)
69 .hint_text("Type group name...")
70 .desired_width(ui.available_width() - 160.0),
71 );
72
73 let focus_id = ui.make_persistent_id("group_input_focus_init");
75 let has_focused = ui.data_mut(|d| d.get_temp::<bool>(focus_id).unwrap_or(false));
76
77 if !has_focused {
78 text_edit_res.request_focus();
79 ui.data_mut(|d| d.insert_temp(focus_id, true));
80 }
81
82 ui.data_mut(|d| d.insert_temp(input_id, buffer.clone()));
83
84 if text_edit_res.ctx.input(|i| i.key_pressed(Key::Enter)) && !buffer.is_empty() {
86 let flag = annotation_groups.iter().any(|group| {
87 if group.name == buffer.trim() {
88 return true;
89 }
90 false
91 });
92
93 if !flag {
94 msgs.push(Message::CreateAnnotationGroup(buffer.trim().to_string()));
95 ui.data_mut(|d| d.insert_temp(input_id, String::new()));
96 }
97 text_edit_res.request_focus();
99 }
100 if ui
102 .button(icons::ADD_LINE)
103 .on_hover_text("Create Group")
104 .clicked()
105 && !buffer.is_empty()
106 {
107 msgs.push(Message::CreateAnnotationGroup(buffer.trim().to_string()));
108 ui.data_mut(|d| d.insert_temp(input_id, String::new()));
109 }
110
111 if ui
113 .button(icons::DELETE_BIN_LINE)
114 .on_hover_text("Delete Group")
115 .clicked()
116 && !buffer.is_empty()
117 {
118 msgs.push(Message::DeleteAnnotationGroup(buffer.trim().to_string()));
119 ui.data_mut(|d| d.insert_temp(input_id, String::new()));
120 }
121 });
122
123 ui.add_space(DEFAULT_SPACE);
124 ui.separator();
125
126 egui::ScrollArea::vertical()
128 .auto_shrink([false; 2])
129 .show(ui, |ui| {
130 for group in annotation_groups.iter_mut().rev() {
132 self.render_group_section(ui, group, msgs, time_formatter);
133 }
134 });
135 }
136
137 fn render_group_section(
138 &self,
139 ui: &mut Ui,
140 group: &mut AnnotationGroup,
141 msgs: &mut Vec<Message>,
142 time_formatter: &TimeFormatter,
143 ) {
144 let any_visible = group.annotations.iter().any(|id| {
146 if let Some(annotation) = self.get_annotation_by_id(id) {
147 annotation.is_visible()
148 } else {
149 warn!("Got id to non existing annotatation");
150 false
151 }
152 });
153 let group_icon = if any_visible {
154 icons::EYE_LINE
155 } else {
156 icons::EYE_OFF_LINE
157 };
158
159 let id = ui.make_persistent_id(&group.name);
161 let state =
162 egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true);
163
164 state
165 .show_header(ui, |ui| {
166 ui.label(format!("{} ({})", group.name, group.annotations.len()));
167
168 let delete_tooltip;
169 let delete_message;
170 if group.annotations.is_empty() {
171 delete_tooltip = "Delete this group";
172 delete_message = Message::DeleteAnnotationGroup(group.name.clone());
173 } else {
174 delete_tooltip = "Delete all annotations in this group";
175 delete_message = Message::DeleteAllAnnotationInGroup(group.name.clone());
176 }
177 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
179 if group.name != DEFAULT_GROUP_NAME
180 && ui
181 .button(icons::DELETE_BIN_LINE)
182 .on_hover_text(delete_tooltip)
183 .clicked()
184 {
185 msgs.push(delete_message);
186 }
187 if ui
188 .button(group_icon)
189 .on_hover_text("Toggle visibility for all in this group")
190 .clicked()
191 {
192 msgs.push(Message::SetGroupVisibility(group.clone(), !any_visible));
193 }
194 if group.annotations.len() > 1
196 && ui
197 .button(icons::SKIP_FORWARD_LINE)
198 .on_hover_text("Cycle through group")
199 .clicked()
200 {
201 msgs.push(Message::GoToAnnotationPosition(
202 group.annotations[group.cycle_counter],
203 self.last_active_viewport_idx,
204 ));
205 group.cycle_counter += 1;
206
207 if group.cycle_counter >= group.annotations.len() {
208 group.cycle_counter = 0;
209 }
210 }
211 });
212 })
213 .body(|ui| {
214 if group.annotations.is_empty() {
215 ui.weak(" No items");
216 }
217
218 for id in &group.annotations {
219 if let Some(annotation) = self.get_annotation_by_id(id) {
220 ui.horizontal(|ui| {
221 ui.add_space(6.0);
222
223 let editing_id =
225 ui.make_persistent_id(("editing_name", annotation.get_name()));
226 let is_editing =
227 ui.data(|d| d.get_temp::<bool>(editing_id).unwrap_or(false));
228
229 let current_name = annotation.get_name();
230
231 if is_editing {
232 let mut buffer = ui.data_mut(|d| {
233 d.get_temp::<String>(editing_id)
234 .unwrap_or_else(|| current_name.clone())
235 });
236
237 let res = ui.add(
238 egui::TextEdit::singleline(&mut buffer).desired_width(120.0),
239 );
240
241 if res.has_focus() {
242 ui.data_mut(|d| d.insert_temp(editing_id, buffer.clone()));
243 }
244
245 if res.lost_focus()
247 || (res.has_focus() && ui.input(|i| i.key_pressed(Key::Enter)))
248 {
249 msgs.push(Message::UpdateAnnotationName(
250 *id,
251 buffer.trim().to_string(),
252 ));
253 ui.data_mut(|d| d.insert_temp(editing_id, false));
254 }
255
256 if ui.data(|d| {
258 d.get_temp::<bool>(
259 ui.make_persistent_id(("focus_req", ¤t_name)),
260 )
261 .unwrap_or(true)
262 }) {
263 res.request_focus();
264 ui.data_mut(|d| {
265 d.insert_temp(
266 ui.make_persistent_id(("focus_req", current_name)),
267 false,
268 )
269 });
270 }
271 } else {
272 let response = ui.add(
274 egui::Label::new(egui::RichText::new(¤t_name).strong())
275 .sense(egui::Sense::click()),
276 );
277 if response.clicked() {
278 ui.data_mut(|d| d.insert_temp(editing_id, true));
279 ui.data_mut(|d| {
280 d.insert_temp(
281 ui.make_persistent_id(("focus_req", current_name)),
282 true,
283 )
284 });
285 }
286 response.on_hover_text("Click to rename");
287 }
288
289 let show_comment_icon = if annotation.show_comments() {
290 icons::ARROW_DOWN_S_LINE
291 } else {
292 icons::ARROW_RIGHT_S_LINE
293 };
294
295 if ui
296 .button(show_comment_icon)
297 .on_hover_text("Show comments")
298 .clicked()
299 {
300 msgs.push(Message::ToggleAnnotationListShowComments(*id));
301 }
302
303 let placeholder = "ungrouped".to_string();
305
306 ui.menu_button(icons::FOLDER_TRANSFER_LINE, |ui| {
307 for group in self.annotation_groups.iter().rev() {
308 if ui
309 .selectable_value(
310 &mut Some(placeholder.clone()),
311 Some(group.name.clone()),
312 group.name.clone(),
313 )
314 .clicked()
315 {
316 msgs.push(Message::UpdateAnnotationGroup(
317 *id,
318 Some(group.name.clone()),
319 ));
320 ui.close();
321 }
322 }
323 })
324 .response
325 .on_hover_text("Change Group");
326
327 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
329 if ui
330 .button(icons::DELETE_BIN_LINE)
331 .on_hover_text("Delete annotation")
332 .clicked()
333 {
334 msgs.push(Message::RemoveAnnotation(*id));
335 }
336
337 let vis_icon = if annotation.is_visible() {
338 icons::EYE_LINE
339 } else {
340 icons::EYE_OFF_LINE
341 };
342 if ui
343 .button(vis_icon)
344 .on_hover_text("Toggle visibility")
345 .clicked()
346 {
347 msgs.push(Message::ToggleAnnotationVisiblility(*id));
348 }
349
350 let comment = annotation.get_comment_box();
351
352 if annotation.is_visible() {
353 let chat_icon = if comment.visible {
354 icons::CHAT_4_LINE
355 } else {
356 icons::CHAT_OFF_LINE
357 };
358
359 if ui
360 .button(chat_icon)
361 .on_hover_text("Toggle comment visibility")
362 .clicked()
363 {
364 msgs.push(Message::ToggleCommentVisibility(*id));
365 }
366 }
367 if ui
368 .button(icons::SEARCH_LINE)
369 .on_hover_text("Go to annotation")
370 .clicked()
371 {
372 msgs.push(Message::GoToAnnotationPosition(
373 *id,
374 self.last_active_viewport_idx,
375 ));
376 }
377 });
378 });
379
380 ui.horizontal(|ui| {
381 ui.add_space(16.0);
382 ui.label(
383 egui::RichText::new(annotation.get_time_info(time_formatter))
384 .size(TIME_FONT_SIZE)
385 .color(Color32::LIGHT_GRAY),
386 )
387 });
388
389 if annotation.show_comments() {
391 let messages = annotation.get_messages();
392 for c in messages {
393 let mut line_left = ui.cursor().left_top();
394 line_left.x += 16.;
395 ui.painter().add(egui::Shape::line_segment(
396 [line_left, ui.cursor().right_top()],
397 egui::Stroke::new(0.5, egui::Color32::WHITE),
398 ));
399 ui.horizontal(|ui| {
400 ui.add_space(18.0); ui.vertical(|ui| {
402 ui.add_space(DEFAULT_SPACE / 2.);
403 ui.set_max_width(ui.available_width() - WIDTH_CONSTRAINT);
404 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
405
406 ui.add(egui::Label::new(c.text.as_str()).wrap());
407 });
408
409 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
410 let response = ui.add_sized(
411 egui::Vec2::new(10.0, 10.0),
412 egui::Button::new(icons::DELETE_BIN_LINE),
413 );
414
415 if response.on_hover_text("Delete message").clicked() {
416 msgs.push(Message::RemoveCommentMessage(
417 annotation.get_id(),
418 c.id,
419 ));
420 }
421 });
422 });
423 }
424 }
425 }
426 }
427 });
428 ui.separator();
429 ui.add_space(DEFAULT_SPACE);
430 }
431
432 pub fn remove_annotation_from_group(&mut self, id_to_remove: egui::Id) -> Option<egui::Id> {
433 for group in &mut self.annotation_groups {
434 if let Some(idx) = group.annotations.iter().position(|&id| id == id_to_remove) {
435 group.cycle_counter = 0;
436 return Some(group.annotations.remove(idx));
437 }
438 }
439
440 None
441 }
442
443 pub fn remove_all_annotations_from_group(&mut self, name: String) {
444 for group in &mut self.annotation_groups {
445 if group.name == name {
446 self.annotations
447 .retain(|annotation| !group.annotations.contains(&annotation.get_id()));
448 group.cycle_counter = 0;
449 group.annotations = Vec::new();
450 }
451 }
452 }
453
454 pub fn delete_group(&mut self, group_name: String) {
455 if let Some(idx) = self
456 .annotation_groups
457 .iter()
458 .position(|group| group.name == group_name)
459 {
460 self.annotation_groups.remove(idx);
461 }
462 }
463
464 pub fn add_annotation_to_group(&mut self, group_name: String, id_to_add: egui::Id) {
465 if let Some(idx) = self
466 .annotation_groups
467 .iter()
468 .position(|group| group.name == group_name)
469 {
470 self.annotation_groups[idx].annotations.push(id_to_add);
471 }
472 }
473
474 #[must_use]
475 pub fn annotation_is_in_group(&self, annotation_id: egui::Id) -> bool {
476 for group in &self.annotation_groups {
477 if group.annotations.contains(&annotation_id) {
478 return true;
479 }
480 }
481
482 false
483 }
484
485 #[must_use]
486 pub fn get_group_from_annotation(&self, annotation_id: egui::Id) -> Option<&AnnotationGroup> {
487 self.annotation_groups
488 .iter()
489 .find(|&group| group.annotations.contains(&annotation_id))
490 .map(|g| g as _)
491 }
492
493 pub fn get_group_from_name(&mut self, group_name: String) -> Option<&mut AnnotationGroup> {
494 self.annotation_groups
495 .iter_mut()
496 .find(|group| group.name == group_name)
497 .map(|g| g as _)
498 }
499}