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
9pub(crate) const 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 let trimmed_buffer = buffer.trim();
86 if text_edit_res.ctx.input(|i| i.key_pressed(Key::Enter)) && !trimmed_buffer.is_empty()
87 {
88 let flag = annotation_groups
89 .iter()
90 .any(|group| group.name == trimmed_buffer);
91
92 if !flag {
93 msgs.push(Message::CreateAnnotationGroup(trimmed_buffer.to_string()));
94 ui.data_mut(|d| d.insert_temp(input_id, String::new()));
95 }
96 text_edit_res.request_focus();
98 }
99 if ui
101 .button(icons::ADD_LINE)
102 .on_hover_text("Create Group")
103 .clicked()
104 && !trimmed_buffer.is_empty()
105 {
106 msgs.push(Message::CreateAnnotationGroup(trimmed_buffer.to_string()));
107 ui.data_mut(|d| d.insert_temp(input_id, String::new()));
108 }
109
110 if ui
112 .button(icons::DELETE_BIN_LINE)
113 .on_hover_text("Delete Group")
114 .clicked()
115 && !trimmed_buffer.is_empty()
116 {
117 msgs.push(Message::DeleteAnnotationGroup(trimmed_buffer.to_string()));
118 ui.data_mut(|d| d.insert_temp(input_id, String::new()));
119 }
120 });
121
122 ui.add_space(DEFAULT_SPACE);
123 ui.separator();
124
125 egui::ScrollArea::vertical()
127 .auto_shrink([false; 2])
128 .show(ui, |ui| {
129 for group in annotation_groups.iter_mut().rev() {
131 self.render_group_section(ui, group, msgs, time_formatter);
132 }
133 });
134 }
135
136 fn render_group_section(
137 &self,
138 ui: &mut Ui,
139 group: &mut AnnotationGroup,
140 msgs: &mut Vec<Message>,
141 time_formatter: &TimeFormatter,
142 ) {
143 let any_visible = group.annotations.iter().any(|id| {
145 if let Some(annotation) = self.get_annotation_by_id(id) {
146 annotation.is_visible()
147 } else {
148 warn!("Got id to non existing annotatation");
149 false
150 }
151 });
152 let group_icon = if any_visible {
153 icons::EYE_LINE
154 } else {
155 icons::EYE_OFF_LINE
156 };
157
158 let id = ui.make_persistent_id(&group.name);
160 let state =
161 egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true);
162
163 state
164 .show_header(ui, |ui| {
165 ui.label(format!("{} ({})", group.name, group.annotations.len()));
166
167 let (delete_tooltip, delete_message) = if group.annotations.is_empty() {
168 (
169 "Delete this group",
170 Message::DeleteAnnotationGroup(group.name.clone()),
171 )
172 } else {
173 (
174 "Delete all annotations in this group",
175 Message::DeleteAllAnnotationInGroup(group.name.clone()),
176 )
177 };
178 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
180 if group.name != DEFAULT_GROUP_NAME
181 && ui
182 .button(icons::DELETE_BIN_LINE)
183 .on_hover_text(delete_tooltip)
184 .clicked()
185 {
186 msgs.push(delete_message);
187 }
188 if ui
189 .button(group_icon)
190 .on_hover_text("Toggle visibility for all in this group")
191 .clicked()
192 {
193 msgs.push(Message::SetGroupVisibility(group.clone(), !any_visible));
194 }
195 if group.annotations.len() > 1
197 && ui
198 .button(icons::SKIP_FORWARD_LINE)
199 .on_hover_text("Cycle through group")
200 .clicked()
201 {
202 msgs.push(Message::GoToAnnotationPosition(
203 group.annotations[group.cycle_counter],
204 self.last_active_viewport_idx,
205 ));
206 group.cycle_counter += 1;
207
208 if group.cycle_counter >= group.annotations.len() {
209 group.cycle_counter = 0;
210 }
211 }
212 });
213 })
214 .body(|ui| {
215 if group.annotations.is_empty() {
216 ui.weak(" No items");
217 }
218
219 for id in &group.annotations {
220 if let Some(annotation) = self.get_annotation_by_id(id) {
221 ui.horizontal(|ui| {
222 ui.add_space(6.0);
223
224 let editing_id =
226 ui.make_persistent_id(("editing_name", annotation.get_name()));
227 let is_editing =
228 ui.data(|d| d.get_temp::<bool>(editing_id).unwrap_or(false));
229
230 let current_name = annotation.get_name();
231
232 if is_editing {
233 let mut buffer = ui.data_mut(|d| {
234 d.get_temp::<String>(editing_id)
235 .unwrap_or_else(|| current_name.clone())
236 });
237
238 let res = ui.add(
239 egui::TextEdit::singleline(&mut buffer).desired_width(120.0),
240 );
241
242 if res.has_focus() {
243 ui.data_mut(|d| d.insert_temp(editing_id, buffer.clone()));
244 }
245
246 if res.lost_focus()
248 || (res.has_focus() && ui.input(|i| i.key_pressed(Key::Enter)))
249 {
250 msgs.push(Message::UpdateAnnotationName(
251 *id,
252 buffer.trim().to_string(),
253 ));
254 ui.data_mut(|d| d.insert_temp(editing_id, false));
255 }
256
257 if ui.data(|d| {
259 d.get_temp::<bool>(
260 ui.make_persistent_id(("focus_req", ¤t_name)),
261 )
262 .unwrap_or(true)
263 }) {
264 res.request_focus();
265 ui.data_mut(|d| {
266 d.insert_temp(
267 ui.make_persistent_id(("focus_req", current_name)),
268 false,
269 )
270 });
271 }
272 } else {
273 let response = ui.add(
275 egui::Label::new(egui::RichText::new(¤t_name).strong())
276 .sense(egui::Sense::click()),
277 );
278 if response.clicked() {
279 ui.data_mut(|d| d.insert_temp(editing_id, true));
280 ui.data_mut(|d| {
281 d.insert_temp(
282 ui.make_persistent_id(("focus_req", current_name)),
283 true,
284 )
285 });
286 }
287 response.on_hover_text("Click to rename");
288 }
289
290 let show_comment_icon = if annotation.show_comments() {
291 icons::ARROW_DOWN_S_LINE
292 } else {
293 icons::ARROW_RIGHT_S_LINE
294 };
295
296 if ui
297 .button(show_comment_icon)
298 .on_hover_text("Show comments")
299 .clicked()
300 {
301 msgs.push(Message::ToggleAnnotationListShowComments(*id));
302 }
303
304 let placeholder = "ungrouped".to_string();
306
307 ui.menu_button(icons::FOLDER_TRANSFER_LINE, |ui| {
308 for group in self.annotation_groups.iter().rev() {
309 if ui
310 .selectable_value(
311 &mut Some(placeholder.clone()),
312 Some(group.name.clone()),
313 group.name.clone(),
314 )
315 .clicked()
316 {
317 msgs.push(Message::UpdateAnnotationGroup(
318 *id,
319 Some(group.name.clone()),
320 ));
321 ui.close();
322 }
323 }
324 })
325 .response
326 .on_hover_text("Change Group");
327
328 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
330 if ui
331 .button(icons::DELETE_BIN_LINE)
332 .on_hover_text("Delete annotation")
333 .clicked()
334 {
335 msgs.push(Message::RemoveAnnotation(*id));
336 }
337
338 let vis_icon = if annotation.is_visible() {
339 icons::EYE_LINE
340 } else {
341 icons::EYE_OFF_LINE
342 };
343 if ui
344 .button(vis_icon)
345 .on_hover_text("Toggle visibility")
346 .clicked()
347 {
348 msgs.push(Message::ToggleAnnotationVisiblility(*id));
349 }
350
351 let comment = annotation.get_comment_box();
352
353 if annotation.is_visible() {
354 let chat_icon = if comment.visible {
355 icons::CHAT_4_LINE
356 } else {
357 icons::CHAT_OFF_LINE
358 };
359
360 if ui
361 .button(chat_icon)
362 .on_hover_text("Toggle comment visibility")
363 .clicked()
364 {
365 msgs.push(Message::ToggleCommentVisibility(*id));
366 }
367 }
368 if ui
369 .button(icons::SEARCH_LINE)
370 .on_hover_text("Go to annotation")
371 .clicked()
372 {
373 msgs.push(Message::GoToAnnotationPosition(
374 *id,
375 self.last_active_viewport_idx,
376 ));
377 }
378 });
379 });
380
381 ui.horizontal(|ui| {
382 ui.add_space(16.0);
383 ui.label(
384 egui::RichText::new(annotation.get_time_info(time_formatter))
385 .size(TIME_FONT_SIZE)
386 .color(Color32::LIGHT_GRAY),
387 )
388 });
389
390 if annotation.show_comments() {
392 let messages = annotation.get_messages();
393 for c in messages {
394 let mut line_left = ui.cursor().left_top();
395 line_left.x += 16.;
396 ui.painter().add(egui::Shape::line_segment(
397 [line_left, ui.cursor().right_top()],
398 egui::Stroke::new(0.5, egui::Color32::WHITE),
399 ));
400 ui.horizontal(|ui| {
401 ui.add_space(18.0); ui.vertical(|ui| {
403 ui.add_space(DEFAULT_SPACE * 0.5);
404 ui.set_max_width(ui.available_width() - WIDTH_CONSTRAINT);
405 ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
406
407 ui.add(egui::Label::new(c.text.as_str()).wrap());
408 });
409
410 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
411 let response = ui.add_sized(
412 egui::Vec2::new(10.0, 10.0),
413 egui::Button::new(icons::DELETE_BIN_LINE),
414 );
415
416 if response.on_hover_text("Delete message").clicked() {
417 msgs.push(Message::RemoveCommentMessage(
418 annotation.get_id(),
419 c.id,
420 ));
421 }
422 });
423 });
424 }
425 }
426 }
427 }
428 });
429 ui.separator();
430 ui.add_space(DEFAULT_SPACE);
431 }
432
433 pub fn remove_annotation_from_group(&mut self, id_to_remove: egui::Id) -> Option<egui::Id> {
434 for group in &mut self.annotation_groups {
435 if let Some(idx) = group.annotations.iter().position(|&id| id == id_to_remove) {
436 group.cycle_counter = 0;
437 return Some(group.annotations.remove(idx));
438 }
439 }
440
441 None
442 }
443
444 pub fn remove_all_annotations_from_group(&mut self, name: &str) {
445 for group in &mut self.annotation_groups {
446 if group.name == name {
447 self.annotations
448 .retain(|annotation| !group.annotations.contains(&annotation.get_id()));
449 group.cycle_counter = 0;
450 group.annotations = Vec::new();
451 }
452 }
453 }
454
455 pub fn delete_group(&mut self, group_name: &str) {
456 if let Some(idx) = self
457 .annotation_groups
458 .iter()
459 .position(|group| group.name == group_name)
460 {
461 self.annotation_groups.remove(idx);
462 }
463 }
464
465 pub fn add_annotation_to_group(&mut self, group_name: &str, id_to_add: egui::Id) {
466 if let Some(idx) = self
467 .annotation_groups
468 .iter()
469 .position(|group| group.name == group_name)
470 {
471 self.annotation_groups[idx].annotations.push(id_to_add);
472 }
473 }
474
475 #[must_use]
476 pub fn annotation_is_in_group(&self, annotation_id: egui::Id) -> bool {
477 for group in &self.annotation_groups {
478 if group.annotations.contains(&annotation_id) {
479 return true;
480 }
481 }
482
483 false
484 }
485
486 #[must_use]
487 pub fn get_group_from_annotation(&self, annotation_id: egui::Id) -> Option<&AnnotationGroup> {
488 self.annotation_groups
489 .iter()
490 .find(|&group| group.annotations.contains(&annotation_id))
491 .map(|g| g as _)
492 }
493
494 pub fn get_group_from_name(&mut self, group_name: &str) -> Option<&mut AnnotationGroup> {
495 self.annotation_groups
496 .iter_mut()
497 .find(|group| group.name == group_name)
498 .map(|g| g as _)
499 }
500}