1mod color_picker;
10mod floating;
11
12use super::categories::cursors::CursorType;
13use crate::drawings::DrawingToolType;
14use crate::icons::{Icon, icons as embedded_icons};
15use crate::localization as l10n;
16use crate::styles::responsive::LayoutContext;
17use crate::theme::ThemeExt;
18use crate::tokens::DESIGN_TOKENS;
19use crate::ui::Control;
20use crate::ui_kit::buttons::IconButton;
21use egui::{Id, Key, Modifiers, Popup, Rect, Sense, Ui, Vec2};
22use std::collections::HashSet;
23
24#[derive(Clone, Copy, Debug)]
26pub struct CursorStateData {
27 pub current_cursor: CursorType,
28 pub eraser_mode: bool,
29 pub values_tooltip: bool,
30}
31
32impl CursorStateData {
33 pub(crate) fn id() -> Id {
35 Id::unique("drawing_toolbar_cursor_state")
36 }
37
38 pub(crate) fn is_active(&self, cursor: CursorType) -> bool {
40 match cursor {
41 CursorType::Eraser => self.eraser_mode,
42 _ => !self.eraser_mode && self.current_cursor == cursor,
43 }
44 }
45}
46
47#[derive(Clone, Debug)]
52pub struct DrawingToolbarDomainState {
53 pub sel_tool: Option<DrawingToolType>,
55 pub magnet_mode: bool,
57 pub magnet_type: MagnetType,
59 pub snap_to_indicators: bool,
61 pub current_cursor_type: CursorType,
63 pub eraser_mode_enabled: bool,
65 pub stay_in_drawing_mode: bool,
67 pub drawing_color: [u8; 4],
69}
70
71use super::{
73 DrawingToolShortcuts, DrawingToolbarAction, DrawingToolbarConfig,
74 categories::{CursorsCategory, ToolCategory, trend_lines::render_registered_tool_submenu},
75 components::{draw_separator_styled, draw_tool_button},
76 data, icons,
77 state::{MagnetType, ToolbarState},
78 submenu_builder::{flyout_id, show_flyout},
79 utilities::{
80 FavoritesButton, HideMenu, LockButton, MagnetButton, MeasureButton, RemoveMenu,
81 StayInDrawingButton, TemplatesButton, ZoomInButton, ZoomOutButton,
82 },
83};
84
85#[derive(Clone, Default)]
87pub struct DrawingToolbar {
88 pub registry: crate::drawings::DrawingToolRegistry,
91 pub state: ToolbarState,
93 pub config: DrawingToolbarConfig,
95 expanded_category_rect: Option<Rect>,
97 hidden: HashSet<Control>,
99 strip_pass: Option<u64>,
101 magnet_hint_open: bool,
103 magnet_rect: Option<Rect>,
105}
106
107impl DrawingToolbar {
108 pub fn new() -> Self {
110 Self::default()
111 }
112
113 pub fn with_config(mut self, config: DrawingToolbarConfig) -> Self {
115 self.config = config;
116 self
117 }
118
119 pub fn set_control_visible(&mut self, control: Control, visible: bool) {
122 crate::ui::app_state::set_control_visible(&mut self.hidden, control, visible);
123 if control == Control::Templates && !visible {
124 self.state.show_template_name_input = false;
125 }
126 }
127
128 pub fn sync_from_domain_state(&mut self, domain: &DrawingToolbarDomainState) {
132 if domain.sel_tool.is_some()
133 || domain.eraser_mode_enabled
134 || domain.current_cursor_type != self.state.current_cursor_type
135 {
136 self.state.zoom_mode_active = false;
137 }
138 self.state.sel_tool = domain.sel_tool;
139 self.state.magnet_mode = domain.magnet_mode;
140 self.state.magnet_type = domain.magnet_type;
141 self.state.snap_to_indicators = domain.snap_to_indicators;
142 self.state.current_cursor_type = domain.current_cursor_type;
143 self.state.eraser_mode_enabled = domain.eraser_mode_enabled;
144 self.state.stay_in_drawing_mode = domain.stay_in_drawing_mode;
145 self.state.drawing_color = domain.drawing_color;
146 }
147
148 pub fn get_domain_state(&self) -> DrawingToolbarDomainState {
150 DrawingToolbarDomainState {
151 sel_tool: self.state.sel_tool,
152 magnet_mode: self.state.magnet_mode,
153 magnet_type: self.state.magnet_type,
154 snap_to_indicators: self.state.snap_to_indicators,
155 current_cursor_type: self.state.current_cursor_type,
156 eraser_mode_enabled: self.state.eraser_mode_enabled,
157 stay_in_drawing_mode: self.state.stay_in_drawing_mode,
158 drawing_color: self.state.drawing_color,
159 }
160 }
161
162 pub fn select_tool(&mut self, tool: DrawingToolType) {
166 if self.registry.availability(tool).enabled {
167 self.state.select_tool(tool);
168 }
169 }
170
171 pub fn add_to_recent(&mut self, tool: DrawingToolType) {
173 self.state.add_to_recent(tool);
174 }
175
176 pub fn clear_selection(&mut self) {
178 self.state.clear_selection();
179 }
180
181 pub fn toggle_favorite(&mut self, tool: DrawingToolType) {
183 self.state.toggle_favorite(tool);
184 }
185
186 pub fn is_favorite(&self, tool: &DrawingToolType) -> bool {
188 self.state.is_favorite(tool)
189 }
190
191 pub fn toggle_magnet(&mut self) {
193 self.state.toggle_magnet();
194 }
195
196 pub fn toggle_stay_in_drawing(&mut self) {
198 self.state.toggle_stay_in_drawing();
199 }
200
201 pub fn show(&mut self, ui: &mut Ui) -> Option<DrawingToolType> {
203 let action = self.show_with_action(ui);
204 match action {
205 DrawingToolbarAction::SelectTool(tool) => Some(tool),
206 _ => None,
207 }
208 }
209
210 pub fn show_with_action(&mut self, ui: &mut Ui) -> DrawingToolbarAction {
212 let mut action = DrawingToolbarAction::None;
213
214 if !crate::widget::keyboard::shortcuts_suppressed(ui.ctx())
216 && !ui.ctx().data(|data| {
217 data.get_temp::<crate::widget::ShortcutMap>(Id::NULL.with("chart_shortcuts"))
218 .is_some()
219 })
220 && !Popup::is_any_open(ui.ctx())
221 && !ui.ctx().text_edit_focused()
222 && let Some(tool) = DrawingToolShortcuts::default().check(ui.ctx())
223 && self.registry.availability(tool).enabled
224 {
225 self.select_tool(tool);
226 action = DrawingToolbarAction::SelectTool(tool);
227 }
228
229 let _layout_ctx = LayoutContext::from_egui(ui.ctx());
231
232 let sidebar_rect = Rect::from_min_size(
233 ui.available_rect_before_wrap().min,
234 Vec2::new(self.config.width, ui.available_height()),
235 );
236
237 {
241 let mut child_ui = ui.new_child(
242 egui::UiBuilder::new()
243 .max_rect(sidebar_rect)
244 .layout(egui::Layout::top_down(egui::Align::Center)),
245 );
246 child_ui.spacing_mut().item_spacing = Vec2::ZERO;
248 child_ui.spacing_mut().scroll.floating = true;
250
251 let content = egui::ScrollArea::vertical()
252 .auto_shrink([false, false])
253 .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded)
254 .show(&mut child_ui, |ui| self.render_toolbar_content(ui))
255 .inner;
256 if content != DrawingToolbarAction::None {
257 action = content;
258 }
259 }
260
261 if self.state.show_color_picker
263 && let Some(new_color) = self.draw_color_picker_popup(ui, sidebar_rect)
264 {
265 self.state.drawing_color = new_color;
266 action = DrawingToolbarAction::ColorChanged(new_color);
267 }
268
269 if self.state.show_template_name_input
271 && let Some(template_action) = TemplatesButton::show_name_dialog(
272 ui,
273 sidebar_rect,
274 &mut self.state.template_name_input,
275 )
276 {
277 self.state.show_template_name_input = false;
278 self.state.template_name_input.clear();
279 if template_action != DrawingToolbarAction::None {
280 action = template_action;
281 }
282 }
283
284 if self.state.search_open {
286 self.show_search(ui, sidebar_rect);
287 }
288
289 if let Some(ref category) = self.state.expanded_category.clone()
291 && let Some(tool_action) = self.draw_submenu(ui, category)
292 {
293 action = tool_action;
294 }
295
296 self.magnet_hint_open &= self.state.magnet_mode;
298 if self.magnet_hint_open
299 && !self.state.magnet_hint_dismissed
300 && self.state.expanded_category.is_none()
301 && let Some(anchor) = self.magnet_rect
302 && MagnetButton::show_hint(ui, anchor)
303 {
304 self.state.magnet_hint_dismissed = true;
305 }
306
307 if let Some(tool) = self.show_floating(ui, sidebar_rect.right_top() + Vec2::splat(8.0)) {
309 action = DrawingToolbarAction::SelectTool(tool);
310 }
311
312 ui.allocate_rect(sidebar_rect, Sense::hover());
313 action
314 }
315
316 fn render_toolbar_content(&mut self, ui: &mut Ui) -> DrawingToolbarAction {
318 let mut action = DrawingToolbarAction::None;
319
320 ui.add_space(self.config.padding);
322
323 if !self.state.search_open
325 && !ui.ctx().data(|data| {
326 data.get_temp::<crate::widget::ShortcutMap>(Id::NULL.with("chart_shortcuts"))
327 .is_some()
328 })
329 && !crate::widget::keyboard::shortcuts_suppressed(ui.ctx())
330 && !Popup::is_any_open(ui.ctx())
331 && !ui.ctx().text_edit_focused()
332 && ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Slash))
333 {
334 self.state.search_open = true;
335 self.state.search_query.clear();
336 ui.memory_mut(|m| m.request_focus(Self::search_id()));
337 }
338
339 action = self.render_cursors_btn(ui, action);
341
342 action = self.render_tool_categories(ui, action);
344
345 draw_separator_styled(ui, self.config.width);
347
348 self.render_bottom_tools(ui, action)
350 }
351
352 fn search_id() -> Id {
353 Id::unique("drawing_toolbar_search")
354 }
355
356 fn show_search(&mut self, ui: &Ui, sidebar_rect: Rect) {
359 let anchor = Rect::from_min_size(
360 sidebar_rect.min,
361 Vec2::new(
362 self.config.width,
363 DESIGN_TOKENS.sizing.toolbar.button_height,
364 ),
365 );
366 let theme = ui.active_theme();
367 let mut open = true;
368 let mut close_clicked = false;
369 let query = &mut self.state.search_query;
370 Popup::new(
371 Id::unique("drawing_toolbar_search_flyout"),
372 ui.ctx().clone(),
373 anchor,
374 ui.layer_id(),
375 )
376 .open_bool(&mut open)
377 .align(egui::RectAlign::RIGHT_START)
378 .gap(theme.spacing.sm)
379 .frame(egui::Frame::menu(ui.style()))
380 .close_behavior(egui::PopupCloseBehavior::IgnoreClicks)
381 .show(|ui| {
382 ui.horizontal(|ui| {
383 egui::TextEdit::singleline(query)
384 .id(Self::search_id())
385 .hint_text(l10n::text(ui.ctx(), "Search tools"))
386 .desired_width(DESIGN_TOKENS.sizing.drawing_toolbar_ext.submenu_width_lg)
387 .show(ui);
388 close_clicked = IconButton::new(&embedded_icons::CLOSE)
389 .tooltip("Close search")
390 .show(ui)
391 .clicked();
392 });
393 });
394 self.state.search_open = open && !close_clicked;
395 if !self.state.search_open {
396 self.state.search_query.clear();
397 }
398 }
399
400 fn render_cursors_btn(
402 &mut self,
403 ui: &mut Ui,
404 action: DrawingToolbarAction,
405 ) -> DrawingToolbarAction {
406 let (cursor_icon, cursor_tooltip): (&Icon, &str) = if self.state.eraser_mode_enabled {
408 (&embedded_icons::ERASER, "Eraser")
409 } else {
410 (
411 self.state.current_cursor_type.icon(),
412 self.state.current_cursor_type.name(),
413 )
414 };
415
416 let is_selected = self.state.sel_tool.is_none() && !self.state.zoom_mode_active;
418
419 let response = draw_tool_button(ui, cursor_icon, cursor_tooltip, is_selected);
421 self.menu_button(ui, "Cursors", &response);
422
423 action
424 }
425
426 fn menu_button(&mut self, ui: &Ui, category: &str, response: &egui::Response) {
429 if response.clicked() {
430 self.toggle_category(ui.ctx(), category, response.rect);
431 }
432 if self.state.expanded_category.as_deref() == Some(category) {
433 self.expanded_category_rect = Some(response.rect);
434 }
435 }
436
437 fn render_tool_categories(
439 &mut self,
440 ui: &mut Ui,
441 action: DrawingToolbarAction,
442 ) -> DrawingToolbarAction {
443 let primary_categories = [
444 "Lines",
445 "Fibonacci",
446 "Patterns",
447 "Projection",
448 "Brushes/Shapes",
449 "Text/Annotations",
450 "Icons/Emojis",
451 ];
452
453 for category in primary_categories {
454 let sections = data::get_category_sections(category);
455 if sections.is_empty() {
456 continue;
457 }
458
459 let mut tools: Vec<DrawingToolType> = sections
460 .iter()
461 .flat_map(|(_, tools)| tools.iter())
462 .copied()
463 .collect();
464
465 tools.retain(|tool| self.registry.availability(*tool).visible);
466
467 if !self.state.search_query.is_empty() {
469 let query = self.state.search_query.to_lowercase();
470 tools.retain(|tool| {
471 tool.as_str().to_lowercase().contains(&query)
472 || l10n::text(ui.ctx(), tool.as_str())
473 .to_lowercase()
474 .contains(&query)
475 });
476 }
477
478 let is_icons = category == "Icons/Emojis";
479 if tools.is_empty() {
480 continue;
481 }
482
483 let display_tool = if is_icons {
485 None
486 } else if let Some(selected) = self.state.sel_tool {
487 if tools.contains(&selected) {
488 Some(selected)
489 } else {
490 self.state
491 .category_last_tool
492 .get(category)
493 .copied()
494 .filter(|tool| tools.contains(tool))
495 .or_else(|| tools.first().copied())
496 }
497 } else {
498 self.state
499 .category_last_tool
500 .get(category)
501 .copied()
502 .filter(|tool| tools.contains(tool))
503 .or_else(|| tools.first().copied())
504 };
505
506 let tool_icon = display_tool
507 .map(icons::get_icon)
508 .unwrap_or(&embedded_icons::EMOJI_ICON);
509
510 let has_sel = self.state.sel_tool.is_some_and(|t| tools.contains(&t));
511
512 let tool_name = display_tool.map(|t| t.as_str().to_string());
513 let tooltip = tool_name.as_deref().unwrap_or(category);
514
515 let response = draw_tool_button(ui, tool_icon, tooltip, has_sel);
517 self.menu_button(ui, category, &response);
518 }
519
520 action
521 }
522
523 fn render_bottom_tools(
525 &mut self,
526 ui: &mut Ui,
527 mut action: DrawingToolbarAction,
528 ) -> DrawingToolbarAction {
529 let width = self.config.width;
530
531 let measure_active = self.state.sel_tool == Some(DrawingToolType::Measure);
533 let measure = self.registry.availability(DrawingToolType::Measure);
534 if measure.visible
535 && let Some(a) = ui
536 .add_enabled_ui(measure.enabled, |ui| {
537 MeasureButton::show(ui, measure_active)
538 })
539 .inner
540 {
541 self.select_tool(DrawingToolType::Measure);
542 action = a;
543 }
544
545 if let Some(a) = ZoomInButton::show(ui, self.state.zoom_mode_active) {
547 self.state.zoom_mode_active = !self.state.zoom_mode_active;
548 action = a;
549 }
550
551 if self.state.has_zoom_history
553 && let Some(a) = ZoomOutButton::show(ui)
554 {
555 action = a;
556 }
557
558 draw_separator_styled(ui, width);
560
561 let response = MagnetButton::show(ui, self.state.magnet_mode);
563 self.magnet_rect = ui.is_rect_visible(response.rect).then_some(response.rect);
564 if response.clicked() {
565 self.state.magnet_mode = !self.state.magnet_mode;
566 self.magnet_hint_open |= self.state.magnet_mode;
567 action = DrawingToolbarAction::ToggleMagnet;
568 }
569 if response.secondary_clicked() {
570 self.toggle_category(ui.ctx(), "Magnet", response.rect);
571 }
572 if self.state.expanded_category.as_deref() == Some("Magnet") {
573 self.expanded_category_rect = Some(response.rect);
574 }
575
576 if let Some(a) = StayInDrawingButton::show(ui, self.state.stay_in_drawing_mode) {
578 self.state.stay_in_drawing_mode = !self.state.stay_in_drawing_mode;
579 action = a;
580 }
581
582 if let Some(a) = LockButton::show(ui, self.state.lock_all) {
584 self.state.lock_all = !self.state.lock_all;
585 action = a;
586 }
587
588 let response = HideMenu::show(ui);
590 self.menu_button(ui, "Hide", &response);
591
592 let strip_shown = self.state.floating_visible && !self.state.favorites.is_empty();
594 if FavoritesButton::show(ui, strip_shown) {
595 self.state.floating_visible = !strip_shown;
596 }
597
598 draw_separator_styled(ui, width);
600
601 let response = RemoveMenu::show(ui);
603 self.menu_button(ui, "Remove", &response);
604
605 action
606 }
607
608 fn toggle_category(&mut self, ctx: &egui::Context, category: &str, rect: Rect) {
610 if self.state.expanded_category.as_deref() == Some(category) {
611 self.close_flyout(ctx);
612 } else {
613 self.state.expanded_category = Some(category.to_string());
614 self.expanded_category_rect = Some(rect);
615 Popup::open_id(ctx, flyout_id(category));
616 }
617 }
618
619 fn close_flyout(&mut self, ctx: &egui::Context) {
620 if let Some(category) = self.state.expanded_category.take() {
621 Popup::close_id(ctx, flyout_id(&category));
622 }
623 self.expanded_category_rect = None;
624 }
625
626 fn draw_submenu(&mut self, ui: &mut Ui, category: &str) -> Option<DrawingToolbarAction> {
629 let anchor = self.expanded_category_rect?;
630 let ctx = ui.ctx().clone();
631 ctx.data_mut(|d| {
632 d.insert_temp(
633 CursorStateData::id(),
634 CursorStateData {
635 current_cursor: self.state.current_cursor_type,
636 eraser_mode: self.state.eraser_mode_enabled,
637 values_tooltip: self.state.values_tooltip_on_long_press,
638 },
639 );
640 });
641
642 let trading = !self.hidden.contains(&Control::Trading);
643 let sel_tool = self.state.sel_tool;
644 let starred = self.state.favorites.len();
645 let state = &mut self.state;
646 let registry = &self.registry;
647 let action = show_flyout(ui, category, anchor, |ui| match category {
648 "Magnet" => MagnetButton::show_submenu(
649 ui,
650 state.magnet_mode,
651 state.magnet_type,
652 state.snap_to_indicators,
653 ),
654 "Hide" => HideMenu::show_submenu(ui, trading, state),
655 "Remove" => RemoveMenu::show_submenu(ui),
656 "Icons/Emojis" => state.glyph_picker.show(ui, registry),
657 _ => {
658 let favorites = &mut state.favorites;
659 let action = if category == "Cursors" {
660 CursorsCategory.render_submenu(ui, sel_tool, favorites)
661 } else {
662 render_registered_tool_submenu(
663 ui,
664 data::get_category_sections(category),
665 sel_tool,
666 favorites,
667 registry,
668 )
669 };
670 (action != DrawingToolbarAction::None).then_some(action)
671 }
672 })
673 .flatten();
674 if self.state.favorites.len() > starred {
676 self.state.floating_visible = true;
677 }
678
679 if !Popup::is_id_open(&ctx, flyout_id(category)) {
681 self.state.expanded_category = None;
682 self.expanded_category_rect = None;
683 }
684 let action = self.apply_pick(action?);
685 let keeps_open = matches!(action, Some(DrawingToolbarAction::SelectTool(_)))
687 && self.state.stay_in_drawing_mode;
688 if !keeps_open {
689 self.close_flyout(&ctx);
690 }
691 action
692 }
693
694 fn apply_pick(&mut self, action: DrawingToolbarAction) -> Option<DrawingToolbarAction> {
697 use DrawingToolbarAction as A;
698 let tool = match &action {
699 A::SelectTool(tool) => Some(*tool),
700 A::InsertEmoji { .. } => Some(DrawingToolType::FontIcon),
701 A::InsertGlyph { icon_name, .. } => Some(if icon_name.starts_with("sticker:") {
702 DrawingToolType::Sticker
703 } else {
704 DrawingToolType::FontIcon
705 }),
706 _ => None,
707 };
708 if tool.is_some_and(|tool| !self.registry.availability(tool).enabled) {
709 return None;
710 }
711 match &action {
712 A::SelectTool(tool) => self.select_tool(*tool),
713 A::SetCursorType(cursor) => {
714 self.clear_selection();
715 self.state.current_cursor_type = *cursor;
716 self.state.eraser_mode_enabled = false;
717 }
718 A::ToggleEraserMode => {
719 self.clear_selection();
720 self.state.eraser_mode_enabled = !self.state.eraser_mode_enabled;
721 }
722 A::ToggleValuesTooltip => {
723 self.state.values_tooltip_on_long_press = !self.state.values_tooltip_on_long_press;
724 }
725 A::SetSnapToIndicators(enabled) => self.state.snap_to_indicators = *enabled,
726 A::SetMagnetType(kind) => {
727 self.state.magnet_type = *kind;
728 self.state.magnet_mode = true;
729 self.magnet_hint_open = true;
730 }
731 A::ToggleMagnet if !self.state.magnet_mode => return None,
734 A::ToggleMagnet => self.state.magnet_mode = false,
735 A::HideAllDrawings => self.state.hide_drawings = !self.state.hide_drawings,
736 A::HideAllIndicators => self.state.hide_indicators = !self.state.hide_indicators,
737 A::HideAllPoss => self.state.hide_positions = !self.state.hide_positions,
738 A::HideAllOrders => self.state.hide_orders = !self.state.hide_orders,
739 A::HideAll => {
740 let state = &mut self.state;
741 state.hide_drawings = true;
742 state.hide_indicators = true;
743 state.hide_positions = true;
744 state.hide_orders = true;
745 }
746 A::InsertEmoji { .. } => self.select_tool(DrawingToolType::FontIcon),
747 A::InsertGlyph { icon_name, .. } => {
748 self.select_tool(if icon_name.starts_with("sticker:") {
749 DrawingToolType::Sticker
750 } else {
751 DrawingToolType::FontIcon
752 })
753 }
754 A::OpenTemplateMenu => {
755 self.state.show_template_name_input = true;
757 self.state.template_name_input.clear();
758 return None;
759 }
760 _ => {}
761 }
762 Some(action)
763 }
764}
765
766#[cfg(test)]
767mod tests {
768 use super::*;
769 use egui::{Context, Event, Key, Modifiers, Pos2, RawInput};
770
771 fn label(shapes: &[egui::Shape], value: &str) -> Option<Rect> {
772 shapes.iter().find_map(|s| match s {
773 egui::Shape::Text(t) if t.galley.job.text == value => Some(t.visual_bounding_rect()),
774 _ => None,
775 })
776 }
777
778 #[test]
781 fn floating_strip_follows_favorites_and_keeps_position_and_visibility() {
782 let ctx = Context::default();
783 let id = Id::NULL.with("floating_drawing_toolbar");
784 let mut toolbar = DrawingToolbar::new();
785 toolbar.state.favorites = vec![
786 DrawingToolType::TrendLine,
787 DrawingToolType::HorizontalLine,
788 DrawingToolType::FibonacciRetracement,
789 ];
790 let frame = |toolbar: &mut DrawingToolbar, at: Pos2| {
791 ctx.run_ui(RawInput::default(), |ui| {
792 toolbar.show_floating(ui, at);
793 })
794 .drop_without_applying_deltas();
795 ctx.memory(|memory| memory.area_rect(id))
796 };
797 let three = frame(&mut toolbar, Pos2::new(20.0, 30.0)).unwrap();
798 assert_eq!(three.min, Pos2::new(20.0, 30.0));
799 toolbar.state.favorites.pop();
800 let two = frame(&mut toolbar, Pos2::new(300.0, 300.0)).unwrap();
801 assert!(
802 two.width() < three.width() - 20.0,
803 "{two:?} after unstarring from {three:?}"
804 );
805 assert_eq!(two.min, three.min, "the strip stays where it was put");
806 assert_eq!(toolbar.state.floating_pos, Some(three.min));
807 toolbar.state.floating_visible = false;
808 let before = ctx.memory(|memory| memory.area_rect(id));
809 frame(&mut toolbar, Pos2::ZERO);
810 assert_eq!(ctx.memory(|memory| memory.area_rect(id)), before);
811 }
812
813 #[test]
815 fn glyph_flyout_searches_places_and_shows_recents() {
816 fn frame(
817 toolbar: &mut DrawingToolbar,
818 ctx: &egui::Context,
819 events: Vec<egui::Event>,
820 ) -> (Option<DrawingToolbarAction>, Vec<egui::Shape>) {
821 let mut action = None;
822 let output = ctx.run_ui(
823 egui::RawInput {
824 events,
825 ..Default::default()
826 },
827 |ui| {
828 action = toolbar.draw_submenu(ui, "Icons/Emojis");
829 },
830 );
831 let shapes = output.shapes.iter().map(|s| s.shape.clone()).collect();
832 output.drop_without_applying_deltas();
833 (action, shapes)
834 }
835 fn click(
836 toolbar: &mut DrawingToolbar,
837 ctx: &egui::Context,
838 at: Pos2,
839 ) -> Option<DrawingToolbarAction> {
840 frame(toolbar, ctx, vec![egui::Event::PointerMoved(at)]);
841 frame(
842 toolbar,
843 ctx,
844 [true, false]
845 .map(|pressed| egui::Event::PointerButton {
846 pos: at,
847 button: egui::PointerButton::Primary,
848 pressed,
849 modifiers: Modifiers::NONE,
850 })
851 .to_vec(),
852 )
853 .0
854 }
855 for dark in [false, true] {
856 for (width, height) in [(240.0, 320.0), (320.0, 400.0), (1440.0, 900.0)] {
857 let ctx = egui::Context::default();
858 ctx.set_visuals(if dark {
859 egui::Visuals::dark()
860 } else {
861 egui::Visuals::light()
862 });
863 l10n::set_translator(&ctx, |english| (english == "Star").then(|| "Etoile".into()));
864 let screen = Rect::from_min_size(Pos2::ZERO, Vec2::new(width, height));
865 ctx.run_ui(
866 RawInput {
867 screen_rect: Some(screen),
868 ..Default::default()
869 },
870 |_| {},
871 )
872 .drop_without_applying_deltas();
873 let mut toolbar = DrawingToolbar::new();
874 toolbar.state.glyph_picker.size = 48.0;
875 let anchor = Rect::from_min_size(Pos2::new(10., 100.), Vec2::splat(40.));
876 let mut previous = "Search icons, emoji and stickers";
877 for (query, glyph) in [
878 ("star", "icon:star"),
879 ("etoile", "icon:star"),
880 ("grinning", "😀"),
881 ("idea", "sticker:idea"),
882 ("", "sticker:idea"),
883 ] {
884 toolbar.toggle_category(&ctx, "Icons/Emojis", anchor);
885 frame(&mut toolbar, &ctx, vec![]);
886 let (_, shapes) = frame(&mut toolbar, &ctx, vec![]);
887 let input =
888 label(&shapes, previous).expect("the flyout must have a search field");
889 assert!(
890 screen.contains_rect(input),
891 "search field outside {screen:?}: {input:?}"
892 );
893 click(&mut toolbar, &ctx, input.center());
894 let modifiers = Modifiers {
895 ctrl: true,
896 command: true,
897 ..Modifiers::NONE
898 };
899 frame(
900 &mut toolbar,
901 &ctx,
902 [true, false]
903 .map(|pressed| egui::Event::Key {
904 key: Key::A,
905 physical_key: None,
906 pressed,
907 repeat: false,
908 modifiers,
909 })
910 .to_vec(),
911 );
912 frame(&mut toolbar, &ctx, vec![egui::Event::Text(query.into())]);
913 if query.is_empty() {
914 frame(
915 &mut toolbar,
916 &ctx,
917 [true, false]
918 .map(|pressed| egui::Event::Key {
919 key: Key::Backspace,
920 physical_key: None,
921 pressed,
922 repeat: false,
923 modifiers: Modifiers::NONE,
924 })
925 .to_vec(),
926 );
927 }
928 let (_, shapes) = frame(&mut toolbar, &ctx, vec![]);
929 if query == "star" {
930 assert!(label(&shapes, "Icons: shapes").is_some());
931 assert!(label(&shapes, "Emoji: symbols").is_some());
932 assert!(label(&shapes, "Emoji: faces").is_none());
933 }
934 if query.is_empty() {
935 assert!(
936 label(&shapes, "Recent").unwrap().top()
937 < label(&shapes, "Icons: signals").unwrap().top()
938 );
939 }
940 let cell = shapes
941 .iter()
942 .find_map(|s| match s {
943 egui::Shape::Rect(r) if r.rect.size() == Vec2::splat(40.0) => {
944 Some(r.rect.center())
945 }
946 _ => None,
947 })
948 .unwrap();
949 assert!(
950 screen.contains(cell),
951 "first glyph outside {screen:?}: {cell:?}"
952 );
953 let action = click(&mut toolbar, &ctx, cell);
954 assert_eq!(
955 action,
956 Some(DrawingToolbarAction::InsertGlyph {
957 icon_name: glyph,
958 size: 48.0
959 })
960 );
961 previous = query;
962 }
963 }
964 }
965 }
966
967 #[test]
969 fn domain_snapshots_restore_magnet_cursor_and_eraser_modes() {
970 for (magnet, cursor, eraser, tool, zoom) in [
971 (
972 MagnetType::Weak,
973 CursorType::Arrow,
974 false,
975 Some(DrawingToolType::HorizontalLine),
976 false,
977 ),
978 (MagnetType::Strong, CursorType::Dot, false, None, false),
979 (MagnetType::Strong, CursorType::Arrow, true, None, false),
980 (MagnetType::Strong, CursorType::Arrow, false, None, true),
981 ] {
982 let mut source = DrawingToolbar::new();
983 source.state.magnet_mode = true;
984 source.state.magnet_type = magnet;
985 source.state.snap_to_indicators = magnet != MagnetType::Weak;
986 source.state.current_cursor_type = cursor;
987 source.state.eraser_mode_enabled = eraser;
988 source.state.sel_tool = tool;
989 let mut target = DrawingToolbar::new();
990 target.select_tool(DrawingToolType::TrendLine);
991 target.clear_selection();
992 target.state.zoom_mode_active = true;
993 target.state.snap_to_indicators = !source.state.snap_to_indicators;
994 target.state.expanded_category = Some("Magnet".into());
995 target.sync_from_domain_state(&source.get_domain_state());
996 assert_eq!(target.state.magnet_type, magnet);
997 assert_eq!(
998 target.state.snap_to_indicators,
999 source.state.snap_to_indicators
1000 );
1001 assert_eq!(target.state.current_cursor_type, cursor);
1002 assert_eq!(target.state.eraser_mode_enabled, eraser);
1003 assert!(target.state.magnet_mode);
1004 assert_eq!(target.state.sel_tool, tool);
1005 assert_eq!(target.state.zoom_mode_active, zoom);
1006 assert_eq!(target.state.expanded_category.as_deref(), Some("Magnet"));
1007 assert_eq!(
1008 target.state.recent.front(),
1009 Some(&DrawingToolType::TrendLine)
1010 );
1011 }
1012 }
1013
1014 #[test]
1016 fn flyout_picks_leave_the_previous_drawing_mode() {
1017 use DrawingToolType::TrendLine;
1018 use DrawingToolbarAction::{SelectTool, SetCursorType, ToggleEraserMode};
1019
1020 let mut toolbar = DrawingToolbar::new();
1021 toolbar.select_tool(TrendLine);
1022 for (action, tool, eraser) in [
1023 (SetCursorType(CursorType::Dot), None, false),
1024 (SelectTool(TrendLine), Some(TrendLine), false),
1025 (ToggleEraserMode, None, true),
1026 (SelectTool(TrendLine), Some(TrendLine), false),
1027 (ToggleEraserMode, None, true),
1028 (ToggleEraserMode, None, false),
1029 ] {
1030 assert_eq!(toolbar.apply_pick(action.clone()), Some(action));
1031 assert_eq!(toolbar.state.sel_tool, tool);
1032 assert_eq!(toolbar.state.eraser_mode_enabled, eraser);
1033 assert_eq!(toolbar.state.current_cursor_type, CursorType::Dot);
1034 }
1035 for enabled in [false, true] {
1036 let mode = toolbar.state.magnet_type;
1037 let on = toolbar.state.magnet_mode;
1038 let action = DrawingToolbarAction::SetSnapToIndicators(enabled);
1039 assert_eq!(toolbar.apply_pick(action.clone()), Some(action));
1040 assert_eq!(toolbar.state.snap_to_indicators, enabled);
1041 assert_eq!(toolbar.state.magnet_type, mode);
1042 assert_eq!(toolbar.state.magnet_mode, on);
1043 }
1044 toolbar.registry.set_availability(
1045 TrendLine,
1046 crate::drawings::ToolAvailability {
1047 visible: false,
1048 enabled: false,
1049 },
1050 );
1051 assert_eq!(toolbar.apply_pick(SelectTool(TrendLine)), None);
1052 let ctx = Context::default();
1053 let output = ctx.run_ui(RawInput::default(), |ui| {
1054 render_registered_tool_submenu(
1055 ui,
1056 data::get_category_sections("Lines"),
1057 None,
1058 &mut vec![],
1059 &toolbar.registry,
1060 );
1061 });
1062 let labels: Vec<_> = output
1063 .shapes
1064 .iter()
1065 .filter_map(|shape| match &shape.shape {
1066 egui::Shape::Text(text) => Some(text.galley.job.text.as_str()),
1067 _ => None,
1068 })
1069 .collect();
1070 assert!(!labels.contains(&TrendLine.as_str()));
1071 assert!(labels.contains(&DrawingToolType::HorizontalLine.as_str()));
1072 output.drop_without_applying_deltas();
1073 }
1074
1075 #[test]
1077 fn magnet_click_switches_it_and_secondary_click_opens_the_strengths() {
1078 use egui::{PointerButton, Pos2, Shape, vec2};
1079 let ctx = Context::default();
1080 let mut toolbar = DrawingToolbar::new();
1081 let frame = |toolbar: &mut DrawingToolbar, events| {
1082 let mut action = DrawingToolbarAction::None;
1083 let mut origin = Pos2::ZERO;
1084 ctx.run_ui(
1085 RawInput {
1086 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(52.0, 1200.0))),
1087 events,
1088 ..Default::default()
1089 },
1090 |ui| {
1091 ui.spacing_mut().item_spacing = Vec2::ZERO;
1092 origin = ui.cursor().min;
1093 action = toolbar.render_toolbar_content(ui);
1094 },
1095 )
1096 .drop_without_applying_deltas();
1097 (action, origin + vec2(26.0, 437.0))
1098 };
1099 let (_, pos) = frame(&mut toolbar, vec![]);
1100 frame(&mut toolbar, vec![Event::PointerMoved(pos)]);
1101 for (button, expected_action, expected_on) in [
1102 (
1103 PointerButton::Primary,
1104 DrawingToolbarAction::ToggleMagnet,
1105 true,
1106 ),
1107 (
1108 PointerButton::Primary,
1109 DrawingToolbarAction::ToggleMagnet,
1110 false,
1111 ),
1112 (PointerButton::Secondary, DrawingToolbarAction::None, false),
1113 ] {
1114 for pressed in [true, false] {
1115 let (action, _) = frame(
1116 &mut toolbar,
1117 vec![Event::PointerButton {
1118 pos,
1119 button,
1120 pressed,
1121 modifiers: Modifiers::NONE,
1122 }],
1123 );
1124 if !pressed {
1125 assert_eq!(action, expected_action);
1126 assert_eq!(toolbar.state.magnet_mode, expected_on);
1127 }
1128 }
1129 }
1130 assert_eq!(toolbar.state.expanded_category.as_deref(), Some("Magnet"));
1131 for pass in 0..2 {
1132 let output = ctx.run_ui(
1133 RawInput {
1134 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(640.0, 1200.0))),
1135 ..Default::default()
1136 },
1137 |ui| {
1138 toolbar.draw_submenu(ui, "Magnet");
1139 },
1140 );
1141 if pass == 1 {
1142 let labels: Vec<_> = output
1143 .shapes
1144 .iter()
1145 .filter_map(|s| match &s.shape {
1146 Shape::Text(text) => Some(text.galley.text()),
1147 _ => None,
1148 })
1149 .collect();
1150 for label in ["Weak magnet", "Strong magnet"] {
1151 assert_eq!(labels.iter().filter(|&&text| text == label).count(), 1);
1152 }
1153 assert!(!labels.contains(&"OHLC magnet"));
1154 }
1155 output.drop_without_applying_deltas();
1156 }
1157 }
1158
1159 #[test]
1162 fn magnet_card_shows_after_the_first_switch_on_until_dismissed() {
1163 use egui::{PointerButton, Shape, vec2};
1164 const EXPLANATION: &str =
1165 "Drawing points snap to the open, high, low or close of the nearest bar.";
1166 let ctx = Context::default();
1167 let frame = |toolbar: &mut DrawingToolbar, events| {
1168 let mut origin = Pos2::ZERO;
1169 let output = ctx.run_ui(
1170 RawInput {
1171 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(640.0, 1200.0))),
1172 events,
1173 ..Default::default()
1174 },
1175 |ui| {
1176 ui.spacing_mut().item_spacing = Vec2::ZERO;
1177 origin = ui.cursor().min;
1178 toolbar.show_with_action(ui);
1179 },
1180 );
1181 let shapes: Vec<_> = output.shapes.iter().map(|s| s.shape.clone()).collect();
1182 output.drop_without_applying_deltas();
1183 (shapes, origin)
1184 };
1185 let click = |toolbar: &mut DrawingToolbar, pos| {
1187 frame(toolbar, vec![Event::PointerMoved(pos)]);
1188 for pressed in [true, false] {
1189 frame(
1190 toolbar,
1191 vec![Event::PointerButton {
1192 pos,
1193 button: PointerButton::Primary,
1194 pressed,
1195 modifiers: Modifiers::NONE,
1196 }],
1197 );
1198 }
1199 frame(toolbar, vec![]).0
1200 };
1201
1202 let mut toolbar = DrawingToolbar::new();
1204 toolbar.sync_from_domain_state(&DrawingToolbarDomainState {
1205 magnet_mode: true,
1206 ..toolbar.get_domain_state()
1207 });
1208 frame(&mut toolbar, vec![]);
1209 let (shapes, origin) = frame(&mut toolbar, vec![]);
1210 assert!(label(&shapes, "Got it").is_none());
1211
1212 let magnet = origin + vec2(26.0, 437.0);
1215 let shapes = click(&mut toolbar, magnet);
1216 assert!(!toolbar.state.magnet_mode);
1217 assert!(label(&shapes, "Got it").is_none());
1218 let shapes = click(&mut toolbar, magnet);
1219 assert!(toolbar.state.magnet_mode);
1220 let text_pos = shapes.iter().find_map(|s| match s {
1221 Shape::Text(t) if t.galley.job.text.starts_with(EXPLANATION) => Some(t.pos),
1222 _ => None,
1223 });
1224 let text_pos = text_pos.unwrap();
1225 assert_eq!(text_pos.x, origin.x + 76.0);
1226 let got_it = label(&shapes, "Got it").unwrap();
1227 let card_centre = (text_pos.y - 20.0 + got_it.center().y + 17.0 + 20.0) / 2.0;
1229 assert!((card_centre - magnet.y).abs() <= 1.0);
1230
1231 let shapes = click(&mut toolbar, got_it.center());
1233 assert!(toolbar.state.magnet_hint_dismissed);
1234 assert!(label(&shapes, "Got it").is_none());
1235 click(&mut toolbar, magnet);
1236 let shapes = click(&mut toolbar, magnet);
1237 assert!(toolbar.state.magnet_mode);
1238 assert!(label(&shapes, "Got it").is_none());
1239
1240 let mut toolbar = DrawingToolbar::new();
1242 toolbar.apply_pick(DrawingToolbarAction::SetMagnetType(MagnetType::Weak));
1243 frame(&mut toolbar, vec![]);
1244 let (shapes, _) = frame(&mut toolbar, vec![]);
1245 assert!(label(&shapes, "Got it").is_some());
1246
1247 toolbar.apply_pick(DrawingToolbarAction::ToggleMagnet);
1249 frame(&mut toolbar, vec![]);
1250 toolbar.sync_from_domain_state(&DrawingToolbarDomainState {
1251 magnet_mode: true,
1252 ..toolbar.get_domain_state()
1253 });
1254 frame(&mut toolbar, vec![]);
1255 let (shapes, _) = frame(&mut toolbar, vec![]);
1256 assert!(label(&shapes, "Got it").is_none());
1257 }
1258
1259 #[test]
1261 fn drawing_bar_icons_keep_their_grid_pitch_and_state_colours() {
1262 use egui::emath::GuiRounding;
1263 use egui::{Color32, Pos2, Shape, vec2};
1264
1265 let mut custom = crate::theme::Theme::dark();
1266 custom.ui.text = Color32::GREEN;
1267 custom.ui.accent_text = Color32::YELLOW;
1268 for (theme, text) in [
1269 (crate::theme::Theme::dark(), Color32::from_gray(219)),
1270 (crate::theme::Theme::light(), Color32::BLACK),
1271 (custom, Color32::GREEN),
1272 ] {
1273 for (selected, magnet) in [
1274 (None, false),
1275 (None, true),
1276 (Some(DrawingToolType::TrendLine), false),
1277 ] {
1278 let ctx = Context::default();
1279 egui_extras::install_image_loaders(&ctx);
1280 crate::theme::apply_to_egui(&ctx, &theme);
1281 let mut toolbar = DrawingToolbar::new();
1282 toolbar.state.sel_tool = selected;
1283 toolbar.state.magnet_mode = magnet;
1284 let mut origin = Pos2::ZERO;
1285 let mut shapes = Vec::new();
1286 for _ in 0..2 {
1287 let mut output = ctx.run_ui(
1288 RawInput {
1289 events: vec![Event::PointerMoved(egui::pos2(26.0, 61.0))],
1290 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(52.0, 1200.0))),
1291 ..Default::default()
1292 },
1293 |ui| {
1294 ui.spacing_mut().item_spacing = Vec2::ZERO;
1295 origin = ui.cursor().min;
1296 toolbar.render_toolbar_content(ui);
1297 },
1298 );
1299 shapes = std::mem::take(&mut output.shapes);
1300 output.drop_without_applying_deltas();
1301 }
1302 let icons: Vec<_> = shapes
1303 .iter()
1304 .filter_map(|s| match &s.shape {
1305 Shape::Rect(r) if r.brush.is_some() => Some((r.rect, r.fill)),
1306 _ => None,
1307 })
1308 .collect();
1309 assert_eq!(icons.len(), 16);
1310 for (i, y) in [
1311 23.0, 61.0, 99.0, 137.0, 175.0, 213.0, 251.0, 289.0, 344.0, 382.0, 437.0,
1312 475.0, 513.0, 551.0, 589.0, 644.0,
1313 ]
1314 .into_iter()
1315 .enumerate()
1316 {
1317 let colour = if (i == 0 && selected.is_none()) || (i == 1 && selected.is_some())
1318 {
1319 text
1320 } else if i == 10 && magnet {
1321 if theme.dark {
1322 theme.ui.panel_bg
1323 } else {
1324 Color32::WHITE
1325 }
1326 } else {
1327 text
1328 };
1329 assert_eq!(
1330 icons[i],
1331 (
1332 Rect::from_center_size(
1333 origin + vec2(26.0, y),
1334 match i {
1335 1 => vec2(29.0, 29.0),
1336 2 => vec2(27.0, 27.0),
1337 0 | 3 | 4 | 8..=11 => vec2(28.0, 28.0),
1338 6 => vec2(24.0, 24.0),
1339 5 => vec2(31.0, 31.0),
1340 7 => vec2(25.0, 25.0),
1341 12..=15 => vec2(26.0, 26.0),
1342 _ => unreachable!(),
1343 }
1344 )
1345 .round_to_pixels(1.0),
1346 colour
1347 ),
1348 "icon {i}, selected {selected:?}"
1349 );
1350 }
1351 let selected_y = if selected.is_none() { 5.5 } else { 43.5 };
1352 let selected_fill = if theme.dark {
1353 theme.ui.btn_bg_active
1354 } else {
1355 Color32::from_gray(235)
1356 };
1357 let hovered_fill = if selected.is_none() {
1358 theme.ui.btn_bg_hover
1359 } else {
1360 selected_fill
1361 };
1362 assert!(shapes.iter().any(|s| matches!(
1363 &s.shape,
1364 Shape::Rect(r) if r.fill == hovered_fill
1365 && r.rect == Rect::from_min_size(origin + vec2(8.5, 43.5), vec2(35.0, 35.0))
1366 )));
1367 assert!(shapes.iter().any(|s| matches!(
1368 &s.shape,
1369 Shape::Rect(r) if r.fill == selected_fill
1370 && r.rect == Rect::from_min_size(origin + vec2(8.5, selected_y), vec2(35.0, 35.0))
1371 )));
1372 if magnet {
1373 let fill = if theme.dark {
1374 theme.ui.text
1375 } else {
1376 Color32::from_gray(26)
1377 };
1378 assert!(shapes.iter().any(|s| matches!(
1379 &s.shape,
1380 Shape::Rect(r) if r.fill == fill
1381 && r.rect == Rect::from_center_size(origin + vec2(26.0, 437.0), vec2(35.0, 35.0))
1382 )));
1383 }
1384 let separator = if theme.dark {
1385 Color32::from_gray(74)
1386 } else {
1387 Color32::from_gray(235)
1388 };
1389 for y in [316.0, 409.0, 616.0] {
1390 assert!(shapes.iter().any(|s| matches!(
1391 &s.shape,
1392 Shape::Rect(r) if r.fill == separator
1393 && r.rect == Rect::from_min_size(origin + vec2(9.0, y), vec2(33.0, 1.0))
1394 )));
1395 }
1396 }
1397 }
1398 }
1399
1400 #[test]
1403 fn a_tool_shortcut_selects_the_tool_unless_a_text_field_has_focus() {
1404 let press_alt_t = |text_focused: bool, modifiers| {
1405 let alt_t = Event::Key {
1406 key: Key::T,
1407 physical_key: None,
1408 pressed: true,
1409 repeat: false,
1410 modifiers,
1411 };
1412 let ctx = Context::default();
1413 let mut toolbar = DrawingToolbar::new();
1414 let mut text = String::new();
1415 let mut frame = |events: Vec<Event>| {
1416 let mut action = DrawingToolbarAction::None;
1417 ctx.run_ui(
1418 RawInput {
1419 events,
1420 ..Default::default()
1421 },
1422 |ui| {
1423 if text_focused {
1424 ui.text_edit_singleline(&mut text).request_focus();
1425 }
1426 action = toolbar.show_with_action(ui);
1427 },
1428 )
1429 .drop_without_applying_deltas();
1430 action
1431 };
1432 frame(vec![]);
1433 frame(vec![alt_t.clone()])
1434 };
1435 assert_eq!(
1436 press_alt_t(false, Modifiers::ALT),
1437 DrawingToolbarAction::SelectTool(DrawingToolType::TrendLine)
1438 );
1439 assert_eq!(
1440 press_alt_t(true, Modifiers::ALT),
1441 DrawingToolbarAction::None
1442 );
1443 assert_eq!(
1444 press_alt_t(false, Modifiers::ALT.plus(Modifiers::SHIFT)),
1445 DrawingToolbarAction::None
1446 );
1447 for enabled in [false, true] {
1448 for key in [Key::T, Key::Slash] {
1449 let ctx = Context::default();
1450 let mut chart = crate::widget::test_frames::chart(&[10.0], &[20.0]);
1451 chart.chart_options.keyboard.enabled = enabled;
1452 if enabled {
1453 chart.shortcuts_mut().bindings.clear();
1454 }
1455 let mut toolbar = DrawingToolbar::new();
1456 ctx.run_ui(
1457 RawInput {
1458 events: vec![Event::Key {
1459 key,
1460 physical_key: None,
1461 pressed: true,
1462 repeat: false,
1463 modifiers: if key == Key::T {
1464 Modifiers::ALT
1465 } else {
1466 Modifiers::NONE
1467 },
1468 }],
1469 ..Default::default()
1470 },
1471 |ui| {
1472 chart.dispatch_shortcuts(ui.ctx());
1473 assert_eq!(toolbar.show_with_action(ui), DrawingToolbarAction::None);
1474 assert!(!toolbar.state.search_open);
1475 assert!(ui.input(|input| input.key_pressed(key)));
1476 },
1477 )
1478 .drop_without_applying_deltas();
1479 }
1480 }
1481 }
1482}