Skip to main content

libsurfer/
toolbar.rs

1//! Toolbar handling.
2use 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
89/// Helper function to add a new toolbar button, setting up icon, hover text etc.
90fn 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    /// Add panel and draw toolbar.
110    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    /// Helper function to help the annotation buttons know what icon to display and message to send.
686    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        // Implementation for drawing the annotation group
702        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        // Explicitly detect drop targets in horizontal gaps between groups.
774        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}