1use egui::{Button, Color32, CursorIcon, Label, Layout, Panel, Rect, RichText, Sense, Stroke, Ui};
3use egui_remixicon::icons;
4use emath::{Align, Vec2};
5
6use std::collections::HashSet;
7
8use crate::message::MessageTarget;
9use crate::mousegestures::AnnotationKind;
10use crate::wave_container::SimulationStatus;
11use crate::wave_source::LoadOptions;
12use crate::{
13 SystemState,
14 file_dialog::OpenMode,
15 message::Message,
16 wave_data::{PER_SCROLL_EVENT, SCROLL_EVENTS_PER_PAGE},
17};
18
19pub(crate) const TOOLBAR_TIME_ID: &str = "toolbar-time";
20
21#[derive(Clone, Copy)]
22pub(crate) struct ToolbarGroupSpec {
23 pub id: &'static str,
24 pub label: &'static str,
25}
26
27const TOOLBAR_GROUP_SPECS: [ToolbarGroupSpec; 12] = [
28 ToolbarGroupSpec {
29 id: "menu",
30 label: "Menu",
31 },
32 ToolbarGroupSpec {
33 id: "files",
34 label: "Files",
35 },
36 ToolbarGroupSpec {
37 id: "copy",
38 label: "Copy",
39 },
40 ToolbarGroupSpec {
41 id: "zoom",
42 label: "Zoom",
43 },
44 ToolbarGroupSpec {
45 id: "navigation",
46 label: "Navigation",
47 },
48 ToolbarGroupSpec {
49 id: "transitions",
50 label: "Transitions",
51 },
52 ToolbarGroupSpec {
53 id: "add_items",
54 label: "Add items",
55 },
56 ToolbarGroupSpec {
57 id: "viewports",
58 label: "Viewports",
59 },
60 ToolbarGroupSpec {
61 id: "undo",
62 label: "Undo/Redo",
63 },
64 ToolbarGroupSpec {
65 id: "cxxrtl",
66 label: "CXXRTL",
67 },
68 ToolbarGroupSpec {
69 id: "time",
70 label: "Time",
71 },
72 ToolbarGroupSpec {
73 id: "annotations",
74 label: "Annotations",
75 },
76];
77
78#[derive(Clone)]
79struct RenderedGroup {
80 row: usize,
81 visible_index: usize,
82 rect: Rect,
83}
84
85pub(crate) fn toolbar_group_specs() -> &'static [ToolbarGroupSpec] {
86 &TOOLBAR_GROUP_SPECS
87}
88
89fn add_toolbar_button(
91 ui: &mut Ui,
92 msgs: &mut Vec<Message>,
93 icon_string: &str,
94 hover_text: &str,
95 message: Message,
96 enabled: bool,
97) {
98 let button = Button::new(RichText::new(icon_string).heading()).frame(false);
99 if ui
100 .add_enabled(enabled, button)
101 .on_hover_text(hover_text)
102 .clicked()
103 {
104 msgs.push(message);
105 }
106}
107
108impl SystemState {
109 pub(crate) fn add_toolbar_panel(&mut self, ui: &mut Ui, msgs: &mut Vec<Message>) {
111 Panel::top("toolbar").show(ui, |ui| {
112 self.draw_toolbar(ui, msgs);
113 });
114 }
115
116 pub(crate) fn toolbar_group_enabled(&self, id: &str) -> bool {
117 self.user
118 .toolbar_group_enabled
119 .get(id)
120 .copied()
121 .flatten()
122 .or_else(|| self.user.config.layout.toolbar_group_visibility(id))
123 .unwrap_or(true)
124 }
125
126 fn default_toolbar_rows(&self) -> Vec<Vec<String>> {
127 let mut rows: Vec<Vec<String>> = Vec::new();
128
129 for spec in TOOLBAR_GROUP_SPECS {
130 let row = usize::from(
131 self.user
132 .config
133 .layout
134 .toolbar_group_row(spec.id)
135 .unwrap_or(0),
136 );
137 while rows.len() <= row {
138 rows.push(Vec::new());
139 }
140 rows[row].push(spec.id.to_string());
141 }
142
143 if rows.is_empty() {
144 rows.push(Vec::new());
145 }
146
147 rows
148 }
149
150 fn ensure_toolbar_rows(&mut self) {
151 if self.user.toolbar_group_rows.is_empty() {
152 self.user.toolbar_group_rows = self.default_toolbar_rows();
153 }
154 }
155
156 fn visible_toolbar_groups(&self) -> HashSet<&'static str> {
157 let mut visible = HashSet::from([
158 "files",
159 "copy",
160 "zoom",
161 "navigation",
162 "transitions",
163 "add_items",
164 "viewports",
165 "undo",
166 "annotations",
167 ]);
168 if !self.show_menu() {
169 visible.insert("menu");
170 }
171 if self
172 .user
173 .waves
174 .as_ref()
175 .and_then(|waves| waves.inner.simulation_status())
176 .is_some()
177 {
178 visible.insert("cxxrtl");
179 }
180 if self.user.waves.is_some() {
181 visible.insert("time");
182 }
183 visible
184 }
185
186 fn active_toolbar_rows(&mut self) -> Vec<Vec<String>> {
187 self.ensure_toolbar_rows();
188
189 let visible = self.visible_toolbar_groups();
190 let mut rows = self.user.toolbar_group_rows.clone();
191 let mut present: HashSet<String> = HashSet::new();
192
193 for row in &mut rows {
194 row.retain(|id| {
195 let is_visible = visible.contains(id.as_str());
196 let is_enabled = self.toolbar_group_enabled(id);
197 let keep = is_visible && is_enabled;
198 if keep {
199 present.insert(id.clone());
200 }
201 keep
202 });
203 }
204
205 rows.retain(|row| !row.is_empty());
206
207 if rows.is_empty() {
208 rows.push(Vec::new());
209 }
210
211 for spec in TOOLBAR_GROUP_SPECS {
212 let id = spec.id.to_string();
213 if visible.contains(spec.id)
214 && self.toolbar_group_enabled(spec.id)
215 && !present.contains(&id)
216 {
217 rows[0].push(id);
218 }
219 }
220
221 rows
222 }
223
224 fn simulate_group_drop(
225 rows: &[Vec<String>],
226 dragged: &str,
227 target_row: usize,
228 target_visible_index: usize,
229 new_row: bool,
230 ) -> Vec<Vec<String>> {
231 let mut next = rows.to_vec();
232 for row in &mut next {
233 row.retain(|id| id != dragged);
234 }
235 next.retain(|row| !row.is_empty());
236
237 let insert_row = if new_row {
238 target_row.min(next.len())
239 } else {
240 target_row.min(next.len().saturating_sub(1))
241 };
242
243 if new_row || next.is_empty() {
244 next.insert(insert_row, vec![dragged.to_string()]);
245 } else {
246 let row = &mut next[insert_row];
247 let index = target_visible_index.min(row.len());
248 row.insert(index, dragged.to_string());
249 }
250
251 next
252 }
253
254 pub(crate) fn set_toolbar_group_row(&mut self, group_id: &str, row: u8) {
255 if !TOOLBAR_GROUP_SPECS.iter().any(|spec| spec.id == group_id) {
256 tracing::warn!(
257 "Unknown toolbar group id '{group_id}' provided to SetToolbarGroupRow (row {row})"
258 );
259 return;
260 }
261
262 self.ensure_toolbar_rows();
263
264 for existing_row in &mut self.user.toolbar_group_rows {
265 existing_row.retain(|id| id != group_id);
266 }
267 self.user
268 .toolbar_group_rows
269 .retain(|existing_row| !existing_row.is_empty());
270
271 let target_row = usize::from(row);
272 while self.user.toolbar_group_rows.len() <= target_row {
273 self.user.toolbar_group_rows.push(Vec::new());
274 }
275 self.user.toolbar_group_rows[target_row].push(group_id.to_string());
276 }
277
278 fn draw_group_container(
279 &mut self,
280 ui: &mut Ui,
281 group_id: &str,
282 msgs: &mut Vec<Message>,
283 wave_loaded: bool,
284 item_and_cursor: bool,
285 ) -> Rect {
286 let stroke = Stroke::new(0.5, self.user.config.theme.border_color);
287 let inner = egui::Frame::default().stroke(stroke).show(ui, |ui| {
288 ui.horizontal(|ui| {
289 let handle = ui
290 .add(Label::new(RichText::new("||").monospace().small()).sense(Sense::drag()));
291 let is_dragging_handle = handle.dragged() || handle.drag_started();
292 if is_dragging_handle {
293 ui.output_mut(|o| o.cursor_icon = CursorIcon::Grabbing);
294 } else if handle.hovered() {
295 ui.output_mut(|o| o.cursor_icon = CursorIcon::Grab);
296 }
297 if handle.drag_started() {
298 self.toolbar_dragging_group = Some(group_id.to_string());
299 }
300
301 match group_id {
302 "menu" => self.draw_toolbar_group_menu(ui, msgs),
303 "files" => self.draw_toolbar_group_files(ui, msgs, wave_loaded),
304 "copy" => self.draw_toolbar_group_copy(ui, msgs, item_and_cursor),
305 "zoom" => self.draw_toolbar_group_zoom(ui, msgs, wave_loaded),
306 "navigation" => self.draw_toolbar_group_navigation(ui, msgs, wave_loaded),
307 "transitions" => self.draw_toolbar_group_transitions(ui, msgs, item_and_cursor),
308 "add_items" => self.draw_toolbar_group_add_items(ui, msgs, wave_loaded),
309 "viewports" => self.draw_toolbar_group_viewports(ui, msgs, wave_loaded),
310 "undo" => self.draw_toolbar_group_history(ui, msgs),
311 "cxxrtl" => self.draw_toolbar_group_simulation(ui, msgs),
312 "time" => self.draw_toolbar_group_time_input(ui, msgs),
313 "annotations" => self.draw_annotation_group(ui, msgs, wave_loaded),
314 _ => {
315 unreachable!("Unknown toolbar group id {group_id:?}")
316 }
317 }
318 });
319 });
320
321 inner.response.rect
322 }
323
324 fn draw_toolbar_group_menu(&self, ui: &mut Ui, msgs: &mut Vec<Message>) {
325 if !self.show_menu() {
326 ui.menu_button(RichText::new(icons::MENU_FILL).heading(), |ui| {
327 self.menu_contents(ui, msgs);
328 });
329 }
330 }
331
332 fn draw_toolbar_group_files(&self, ui: &mut Ui, msgs: &mut Vec<Message>, wave_loaded: bool) {
333 add_toolbar_button(
334 ui,
335 msgs,
336 icons::FOLDER_OPEN_FILL,
337 "Open file...",
338 Message::OpenFileDialog(OpenMode::Open),
339 true,
340 );
341 add_toolbar_button(
342 ui,
343 msgs,
344 icons::DOWNLOAD_CLOUD_FILL,
345 "Open URL...",
346 Message::SetUrlEntryVisible(
347 true,
348 Some(Box::new(|url: String| {
349 Message::LoadWaveformFileFromUrl(url.clone(), LoadOptions::Clear)
350 })),
351 ),
352 true,
353 );
354 add_toolbar_button(
355 ui,
356 msgs,
357 icons::REFRESH_LINE,
358 "Reload",
359 Message::ReloadWaveform(self.user.config.behavior.keep_during_reload),
360 wave_loaded,
361 );
362 add_toolbar_button(
363 ui,
364 msgs,
365 icons::RUN_LINE,
366 "Run command file...",
367 Message::OpenCommandFileDialog,
368 true,
369 );
370 if self.user.surver_url.is_some() {
371 add_toolbar_button(
372 ui,
373 msgs,
374 icons::FILE_LIST_FILL,
375 "Select Surver file",
376 Message::SetSurverFileWindowVisible(true),
377 true,
378 );
379 }
380 }
381
382 fn draw_toolbar_group_copy(&self, ui: &mut Ui, msgs: &mut Vec<Message>, enabled: bool) {
383 add_toolbar_button(
384 ui,
385 msgs,
386 icons::FILE_COPY_FILL,
387 "Copy variable value",
388 Message::VariableValueToClipbord(MessageTarget::CurrentSelection),
389 enabled,
390 );
391 }
392
393 fn draw_toolbar_group_zoom(&self, ui: &mut Ui, msgs: &mut Vec<Message>, wave_loaded: bool) {
394 let viewport_idx = self
395 .user
396 .waves
397 .as_ref()
398 .map_or(0, |waves| waves.last_active_viewport_idx);
399 let cursor_set = self
400 .user
401 .waves
402 .as_ref()
403 .is_some_and(|waves| waves.cursor.is_some());
404 add_toolbar_button(
405 ui,
406 msgs,
407 icons::ZOOM_IN_FILL,
408 "Zoom in",
409 Message::CanvasZoom {
410 mouse_ptr: None,
411 delta: 0.5,
412 viewport_idx,
413 },
414 wave_loaded,
415 );
416 add_toolbar_button(
417 ui,
418 msgs,
419 icons::ZOOM_OUT_FILL,
420 "Zoom out",
421 Message::CanvasZoom {
422 mouse_ptr: None,
423 delta: 2.0,
424 viewport_idx,
425 },
426 wave_loaded,
427 );
428 add_toolbar_button(
429 ui,
430 msgs,
431 icons::TARGET_FILL,
432 "Zoom in on cursor",
433 Message::ZoomToCursor {
434 delta: 0.5,
435 viewport_idx,
436 },
437 wave_loaded && cursor_set,
438 );
439 add_toolbar_button(
440 ui,
441 msgs,
442 icons::ASPECT_RATIO_FILL,
443 "Zoom to fit",
444 Message::ZoomToFit { viewport_idx },
445 wave_loaded,
446 );
447 }
448
449 fn draw_toolbar_group_navigation(
450 &self,
451 ui: &mut Ui,
452 msgs: &mut Vec<Message>,
453 wave_loaded: bool,
454 ) {
455 let viewport_idx = self
456 .user
457 .waves
458 .as_ref()
459 .map_or(0, |waves| waves.last_active_viewport_idx);
460 add_toolbar_button(
461 ui,
462 msgs,
463 icons::REWIND_START_FILL,
464 "Go to start",
465 Message::GoToStart { viewport_idx },
466 wave_loaded,
467 );
468 add_toolbar_button(
469 ui,
470 msgs,
471 icons::REWIND_FILL,
472 "Go one page left",
473 Message::CanvasScroll {
474 delta: Vec2 {
475 y: PER_SCROLL_EVENT * SCROLL_EVENTS_PER_PAGE,
476 x: 0.,
477 },
478 viewport_idx,
479 },
480 wave_loaded,
481 );
482 add_toolbar_button(
483 ui,
484 msgs,
485 icons::PLAY_REVERSE_FILL,
486 "Go left",
487 Message::CanvasScroll {
488 delta: Vec2 {
489 y: PER_SCROLL_EVENT,
490 x: 0.,
491 },
492 viewport_idx,
493 },
494 wave_loaded,
495 );
496 add_toolbar_button(
497 ui,
498 msgs,
499 icons::PLAY_FILL,
500 "Go right",
501 Message::CanvasScroll {
502 delta: Vec2 {
503 y: -PER_SCROLL_EVENT,
504 x: 0.,
505 },
506 viewport_idx,
507 },
508 wave_loaded,
509 );
510 add_toolbar_button(
511 ui,
512 msgs,
513 icons::SPEED_FILL,
514 "Go one page right",
515 Message::CanvasScroll {
516 delta: Vec2 {
517 y: -PER_SCROLL_EVENT * SCROLL_EVENTS_PER_PAGE,
518 x: 0.,
519 },
520 viewport_idx,
521 },
522 wave_loaded,
523 );
524 add_toolbar_button(
525 ui,
526 msgs,
527 icons::FORWARD_END_FILL,
528 "Go to end",
529 Message::GoToEnd { viewport_idx },
530 wave_loaded,
531 );
532 }
533
534 fn draw_toolbar_group_transitions(&self, ui: &mut Ui, msgs: &mut Vec<Message>, enabled: bool) {
535 add_toolbar_button(
536 ui,
537 msgs,
538 icons::CONTRACT_LEFT_FILL,
539 "Set cursor on previous transition of focused variable",
540 Message::MoveCursorToTransition {
541 next: false,
542 variable: None,
543 skip_zero: false,
544 },
545 enabled,
546 );
547 add_toolbar_button(
548 ui,
549 msgs,
550 icons::CONTRACT_RIGHT_FILL,
551 "Set cursor on next transition of focused variable",
552 Message::MoveCursorToTransition {
553 next: true,
554 variable: None,
555 skip_zero: false,
556 },
557 enabled,
558 );
559 }
560
561 fn draw_toolbar_group_add_items(
562 &self,
563 ui: &mut Ui,
564 msgs: &mut Vec<Message>,
565 wave_loaded: bool,
566 ) {
567 add_toolbar_button(
568 ui,
569 msgs,
570 icons::SPACE,
571 "Add divider",
572 Message::AddDivider(None, None),
573 wave_loaded,
574 );
575 add_toolbar_button(
576 ui,
577 msgs,
578 icons::TIME_FILL,
579 "Add timeline",
580 Message::AddTimeLine(None),
581 wave_loaded,
582 );
583 }
584
585 fn draw_toolbar_group_viewports(
586 &self,
587 ui: &mut Ui,
588 msgs: &mut Vec<Message>,
589 wave_loaded: bool,
590 ) {
591 let multiple_viewports = self
592 .user
593 .waves
594 .as_ref()
595 .is_some_and(|waves| waves.viewports.len() > 1);
596
597 add_toolbar_button(
598 ui,
599 msgs,
600 icons::ADD_BOX_FILL,
601 "Add viewport",
602 Message::AddViewport,
603 wave_loaded,
604 );
605 add_toolbar_button(
606 ui,
607 msgs,
608 icons::CHECKBOX_INDETERMINATE_FILL,
609 "Remove viewport",
610 Message::RemoveViewport,
611 wave_loaded && multiple_viewports,
612 );
613 }
614
615 fn draw_toolbar_group_history(&self, ui: &mut Ui, msgs: &mut Vec<Message>) {
616 let undo_available = !self.undo_stack.is_empty();
617 let redo_available = !self.redo_stack.is_empty();
618
619 let undo_tooltip = if let Some(undo_op) = self.undo_stack.last() {
620 format!("Undo: {}", undo_op.message)
621 } else {
622 "Undo".into()
623 };
624 let redo_tooltip = if let Some(redo_op) = self.redo_stack.last() {
625 format!("Redo: {}", redo_op.message)
626 } else {
627 "Redo".into()
628 };
629 add_toolbar_button(
630 ui,
631 msgs,
632 icons::ARROW_GO_BACK_FILL,
633 &undo_tooltip,
634 Message::Undo(1),
635 undo_available,
636 );
637 add_toolbar_button(
638 ui,
639 msgs,
640 icons::ARROW_GO_FORWARD_FILL,
641 &redo_tooltip,
642 Message::Redo(1),
643 redo_available,
644 );
645 }
646
647 fn draw_toolbar_group_simulation(&self, ui: &mut Ui, msgs: &mut Vec<Message>) {
648 let Some(waves) = &self.user.waves else {
649 return;
650 };
651 let Some(status) = waves.inner.simulation_status() else {
652 return;
653 };
654
655 ui.label("Simulation");
656 match status {
657 SimulationStatus::Paused => add_toolbar_button(
658 ui,
659 msgs,
660 icons::PLAY_CIRCLE_FILL,
661 "Run simulation",
662 Message::UnpauseSimulation,
663 true,
664 ),
665 SimulationStatus::Running => add_toolbar_button(
666 ui,
667 msgs,
668 icons::PAUSE_CIRCLE_FILL,
669 "Pause simulation",
670 Message::PauseSimulation,
671 true,
672 ),
673 SimulationStatus::Finished => {
674 ui.label("Finished");
675 }
676 }
677 }
678
679 fn draw_toolbar_group_time_input(&self, ui: &mut Ui, msgs: &mut Vec<Message>) {
680 if let Some(waves) = &self.user.waves {
681 self.time_input_widget(ui, TOOLBAR_TIME_ID, waves, msgs);
682 }
683 }
684
685 fn annotation_helper<'a>(
687 &self,
688 hover_text: &'a str,
689 icon_unselected: &'a str,
690 icon_selected: &'a str,
691 annotation_kind: AnnotationKind,
692 ) -> (&'a str, Option<AnnotationKind>, &'a str) {
693 if self.annotation_kind == Some(annotation_kind) {
694 (icon_selected, None, "Cancel Action")
695 } else {
696 (icon_unselected, Some(annotation_kind), hover_text)
697 }
698 }
699
700 fn draw_annotation_group(&mut self, ui: &mut Ui, msgs: &mut Vec<Message>, wave_loaded: bool) {
701 let (rect_icon, rect_kind, rect_text) = self.annotation_helper(
703 "Add Rectangle",
704 icons::EDIT_BOX_LINE,
705 icons::EDIT_BOX_FILL,
706 AnnotationKind::Rectangle,
707 );
708 add_toolbar_button(
709 ui,
710 msgs,
711 rect_icon,
712 rect_text,
713 Message::SetMouseGestureAnnotation(rect_kind),
714 wave_loaded,
715 );
716 let (arrow_icon, arrow_kind, arrow_text) = self.annotation_helper(
717 "Add Arrow",
718 icons::ARROW_RIGHT_UP_BOX_LINE,
719 icons::ARROW_RIGHT_UP_BOX_FILL,
720 AnnotationKind::ArrowSingleHead,
721 );
722 add_toolbar_button(
723 ui,
724 msgs,
725 arrow_icon,
726 arrow_text,
727 Message::SetMouseGestureAnnotation(arrow_kind),
728 wave_loaded,
729 );
730
731 let (double_arrow_icon, double_arrow_kind, double_arrow_text) = self.annotation_helper(
732 "Add Double Headed Arrow",
733 icons::ARROW_LEFT_RIGHT_FILL,
734 icons::CLOSE_LARGE_LINE,
735 AnnotationKind::ArrowDoubleHead,
736 );
737 add_toolbar_button(
738 ui,
739 msgs,
740 double_arrow_icon,
741 double_arrow_text,
742 Message::SetMouseGestureAnnotation(double_arrow_kind),
743 wave_loaded,
744 );
745
746 add_toolbar_button(
747 ui,
748 msgs,
749 icons::LIST_CHECK,
750 "Annotations list",
751 Message::ToggleAnnotationlistVisibility(),
752 wave_loaded,
753 );
754 }
755
756 fn compute_drop_target(
757 pointer: egui::Pos2,
758 rows: &[Vec<String>],
759 row_rects: &[Rect],
760 rendered: &[RenderedGroup],
761 ) -> Option<(usize, usize, bool)> {
762 for group in rendered {
763 if group.rect.contains(pointer) {
764 let idx = if pointer.x > group.rect.center().x {
765 group.visible_index + 1
766 } else {
767 group.visible_index
768 };
769 return Some((group.row, idx, false));
770 }
771 }
772
773 for (row_idx, row_rect) in row_rects.iter().enumerate() {
775 if !row_rect.contains(pointer) {
776 continue;
777 }
778
779 let mut row_groups: Vec<&RenderedGroup> =
780 rendered.iter().filter(|g| g.row == row_idx).collect();
781 row_groups.sort_by_key(|g| g.visible_index);
782 if row_groups.is_empty() {
783 continue;
784 }
785
786 if pointer.x <= row_groups[0].rect.left() {
787 return Some((row_idx, 0, false));
788 }
789
790 for pair in row_groups.windows(2) {
791 let left = pair[0];
792 let right = pair[1];
793 if pointer.x >= left.rect.right() && pointer.x <= right.rect.left() {
794 return Some((row_idx, right.visible_index, false));
795 }
796 }
797
798 if pointer.x >= row_groups[row_groups.len() - 1].rect.right() {
799 return Some((row_idx, row_groups.len(), false));
800 }
801 }
802
803 if row_rects.is_empty() {
804 return Some((0, 0, true));
805 }
806 if pointer.y < row_rects[0].top() {
807 return Some((0, 0, true));
808 }
809 if pointer.y > row_rects[row_rects.len() - 1].bottom() {
810 return Some((rows.len(), 0, true));
811 }
812 for (i, pair) in row_rects.windows(2).enumerate() {
813 if pointer.y > pair[0].bottom() && pointer.y < pair[1].top() {
814 return Some((i + 1, 0, true));
815 }
816 }
817
818 let nearest_row = row_rects
819 .iter()
820 .enumerate()
821 .min_by(|(_, a), (_, b)| {
822 (a.center().y - pointer.y)
823 .abs()
824 .total_cmp(&(b.center().y - pointer.y).abs())
825 })
826 .map_or(0, |(idx, _)| idx);
827
828 let mut centers: Vec<f32> = rendered
829 .iter()
830 .filter(|g| g.row == nearest_row)
831 .map(|g| g.rect.center().x)
832 .collect();
833 centers.sort_by(f32::total_cmp);
834 let visible_index = centers.iter().take_while(|x| pointer.x > **x).count();
835
836 Some((nearest_row, visible_index, false))
837 }
838
839 fn apply_toolbar_drop(&mut self, rows: &[Vec<String>]) {
840 let Some(dragged) = self.toolbar_dragging_group.clone() else {
841 return;
842 };
843 let Some(target_row) = self.toolbar_drop_row else {
844 return;
845 };
846 let Some(target_index) = self.toolbar_drop_index else {
847 return;
848 };
849
850 self.user.toolbar_group_rows = Self::simulate_group_drop(
851 rows,
852 &dragged,
853 target_row,
854 target_index,
855 self.toolbar_drop_new_row,
856 );
857 }
858
859 fn clear_toolbar_drag_state(&mut self) {
860 self.toolbar_dragging_group = None;
861 self.toolbar_drop_row = None;
862 self.toolbar_drop_index = None;
863 self.toolbar_drop_new_row = false;
864 }
865
866 fn draw_drop_indicator(
867 &self,
868 ui: &Ui,
869 row_rects: &[Rect],
870 rendered: &[RenderedGroup],
871 row: usize,
872 visible_index: usize,
873 new_row: bool,
874 ) {
875 let indicator_stroke = Stroke::new(3.0, Color32::from_white_alpha(210));
876
877 if new_row {
878 let y = if row == 0 {
879 row_rects.first().map_or(0.0, Rect::top)
880 } else if row >= row_rects.len() {
881 row_rects.last().map_or(0.0, Rect::bottom)
882 } else {
883 row_rects[row - 1].bottom().midpoint(row_rects[row].top())
884 };
885
886 let x_min = row_rects
887 .iter()
888 .map(Rect::left)
889 .min_by(f32::total_cmp)
890 .unwrap_or(0.0);
891 let x_max = row_rects
892 .iter()
893 .map(Rect::right)
894 .max_by(f32::total_cmp)
895 .unwrap_or(x_min + 40.0);
896
897 ui.painter().line_segment(
898 [egui::pos2(x_min, y), egui::pos2(x_max, y)],
899 indicator_stroke,
900 );
901 return;
902 }
903
904 let Some(row_rect) = row_rects.get(row) else {
905 return;
906 };
907
908 let mut row_groups: Vec<&RenderedGroup> =
909 rendered.iter().filter(|g| g.row == row).collect();
910 row_groups.sort_by_key(|g| g.visible_index);
911 if row_groups.is_empty() {
912 return;
913 }
914
915 let x = if visible_index == 0 {
916 row_groups[0].rect.left()
917 } else if visible_index >= row_groups.len() {
918 row_groups[row_groups.len() - 1].rect.right()
919 } else {
920 (row_groups[visible_index - 1].rect.right() + row_groups[visible_index].rect.left())
921 * 0.5
922 };
923
924 ui.painter().line_segment(
925 [
926 egui::pos2(x, row_rect.top()),
927 egui::pos2(x, row_rect.bottom()),
928 ],
929 indicator_stroke,
930 );
931 }
932
933 fn draw_toolbar(&mut self, ui: &mut Ui, msgs: &mut Vec<Message>) {
934 let wave_loaded = self.user.waves.is_some();
935
936 let (item_selected, cursor_set) = if let Some(waves) = &self.user.waves {
937 (waves.focused_item.is_some(), waves.cursor.is_some())
938 } else {
939 (false, false)
940 };
941 let item_and_cursor = item_selected && cursor_set;
942
943 let active_rows = self.active_toolbar_rows();
944 let mut rendered_groups = Vec::<RenderedGroup>::new();
945 let mut row_rects = Vec::<Rect>::new();
946
947 ui.with_layout(Layout::top_down(Align::Min), |ui| {
948 for (row_idx, row) in active_rows.iter().enumerate() {
949 let response = ui.horizontal(|ui| {
950 for (visible_idx, group_id) in row.iter().enumerate() {
951 let rect = self.draw_group_container(
952 ui,
953 group_id,
954 msgs,
955 wave_loaded,
956 item_and_cursor,
957 );
958 rendered_groups.push(RenderedGroup {
959 row: row_idx,
960 visible_index: visible_idx,
961 rect,
962 });
963 }
964 });
965 row_rects.push(response.response.rect);
966 }
967 });
968
969 let dragging_group_visible = self
970 .toolbar_dragging_group
971 .as_deref()
972 .is_some_and(|dragging_id| active_rows.iter().flatten().any(|id| id == dragging_id));
973
974 if dragging_group_visible {
975 if let Some(pointer) = ui.ctx().pointer_hover_pos()
976 && let Some((row, idx, new_row)) =
977 Self::compute_drop_target(pointer, &active_rows, &row_rects, &rendered_groups)
978 {
979 self.toolbar_drop_row = Some(row);
980 self.toolbar_drop_index = Some(idx);
981 self.toolbar_drop_new_row = new_row;
982
983 self.draw_drop_indicator(ui, &row_rects, &rendered_groups, row, idx, new_row);
984 }
985
986 if ui.input(|i| i.pointer.any_released()) {
987 self.apply_toolbar_drop(&active_rows);
988 self.clear_toolbar_drag_state();
989 }
990 }
991 }
992}