context_menu.rs

  1use crate::{
  2    h_flex, prelude::*, utils::WithRemSize, v_flex, Icon, IconName, IconSize, KeyBinding, Label,
  3    List, ListItem, ListSeparator, ListSubHeader,
  4};
  5use gpui::{
  6    px, Action, AnyElement, App, AppContext as _, DismissEvent, Entity, EventEmitter, FocusHandle,
  7    Focusable, IntoElement, Render, Subscription,
  8};
  9use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
 10use settings::Settings;
 11use std::{rc::Rc, time::Duration};
 12use theme::ThemeSettings;
 13
 14pub enum ContextMenuItem {
 15    Separator,
 16    Header(SharedString),
 17    Label(SharedString),
 18    Entry(ContextMenuEntry),
 19    CustomEntry {
 20        entry_render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
 21        handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
 22        selectable: bool,
 23    },
 24}
 25
 26impl ContextMenuItem {
 27    pub fn custom_entry(
 28        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
 29        handler: impl Fn(&mut Window, &mut App) + 'static,
 30    ) -> Self {
 31        Self::CustomEntry {
 32            entry_render: Box::new(entry_render),
 33            handler: Rc::new(move |_, window, cx| handler(window, cx)),
 34            selectable: true,
 35        }
 36    }
 37}
 38
 39pub struct ContextMenuEntry {
 40    toggle: Option<(IconPosition, bool)>,
 41    label: SharedString,
 42    icon: Option<IconName>,
 43    icon_position: IconPosition,
 44    icon_size: IconSize,
 45    icon_color: Option<Color>,
 46    handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
 47    action: Option<Box<dyn Action>>,
 48    disabled: bool,
 49    documentation_aside: Option<Rc<dyn Fn(&mut App) -> AnyElement>>,
 50}
 51
 52impl ContextMenuEntry {
 53    pub fn new(label: impl Into<SharedString>) -> Self {
 54        ContextMenuEntry {
 55            toggle: None,
 56            label: label.into(),
 57            icon: None,
 58            icon_position: IconPosition::Start,
 59            icon_size: IconSize::Small,
 60            icon_color: None,
 61            handler: Rc::new(|_, _, _| {}),
 62            action: None,
 63            disabled: false,
 64            documentation_aside: None,
 65        }
 66    }
 67
 68    pub fn toggleable(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
 69        self.toggle = Some((toggle_position, toggled));
 70        self
 71    }
 72
 73    pub fn icon(mut self, icon: IconName) -> Self {
 74        self.icon = Some(icon);
 75        self
 76    }
 77
 78    pub fn icon_position(mut self, position: IconPosition) -> Self {
 79        self.icon_position = position;
 80        self
 81    }
 82
 83    pub fn icon_size(mut self, icon_size: IconSize) -> Self {
 84        self.icon_size = icon_size;
 85        self
 86    }
 87
 88    pub fn icon_color(mut self, icon_color: Color) -> Self {
 89        self.icon_color = Some(icon_color);
 90        self
 91    }
 92
 93    pub fn toggle(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
 94        self.toggle = Some((toggle_position, toggled));
 95        self
 96    }
 97
 98    pub fn action(mut self, action: Box<dyn Action>) -> Self {
 99        self.action = Some(action);
100        self
101    }
102
103    pub fn handler(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
104        self.handler = Rc::new(move |_, window, cx| handler(window, cx));
105        self
106    }
107
108    pub fn disabled(mut self, disabled: bool) -> Self {
109        self.disabled = disabled;
110        self
111    }
112
113    pub fn documentation_aside(
114        mut self,
115        element: impl Fn(&mut App) -> AnyElement + 'static,
116    ) -> Self {
117        self.documentation_aside = Some(Rc::new(element));
118        self
119    }
120}
121
122impl From<ContextMenuEntry> for ContextMenuItem {
123    fn from(entry: ContextMenuEntry) -> Self {
124        ContextMenuItem::Entry(entry)
125    }
126}
127
128pub struct ContextMenu {
129    items: Vec<ContextMenuItem>,
130    focus_handle: FocusHandle,
131    action_context: Option<FocusHandle>,
132    selected_index: Option<usize>,
133    delayed: bool,
134    clicked: bool,
135    _on_blur_subscription: Subscription,
136    keep_open_on_confirm: bool,
137    documentation_aside: Option<(usize, Rc<dyn Fn(&mut App) -> AnyElement>)>,
138}
139
140impl Focusable for ContextMenu {
141    fn focus_handle(&self, _cx: &App) -> FocusHandle {
142        self.focus_handle.clone()
143    }
144}
145
146impl EventEmitter<DismissEvent> for ContextMenu {}
147
148impl FluentBuilder for ContextMenu {}
149
150impl ContextMenu {
151    pub fn build(
152        window: &mut Window,
153        cx: &mut App,
154        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
155    ) -> Entity<Self> {
156        cx.new(|cx| {
157            let focus_handle = cx.focus_handle();
158            let _on_blur_subscription = cx.on_blur(
159                &focus_handle,
160                window,
161                |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
162            );
163            window.refresh();
164            f(
165                Self {
166                    items: Default::default(),
167                    focus_handle,
168                    action_context: None,
169                    selected_index: None,
170                    delayed: false,
171                    clicked: false,
172                    _on_blur_subscription,
173                    keep_open_on_confirm: false,
174                    documentation_aside: None,
175                },
176                window,
177                cx,
178            )
179        })
180    }
181
182    pub fn context(mut self, focus: FocusHandle) -> Self {
183        self.action_context = Some(focus);
184        self
185    }
186
187    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
188        self.items.push(ContextMenuItem::Header(title.into()));
189        self
190    }
191
192    pub fn separator(mut self) -> Self {
193        self.items.push(ContextMenuItem::Separator);
194        self
195    }
196
197    pub fn extend<I: Into<ContextMenuItem>>(mut self, items: impl IntoIterator<Item = I>) -> Self {
198        self.items.extend(items.into_iter().map(Into::into));
199        self
200    }
201
202    pub fn item(mut self, item: impl Into<ContextMenuItem>) -> Self {
203        self.items.push(item.into());
204        self
205    }
206
207    pub fn entry(
208        mut self,
209        label: impl Into<SharedString>,
210        action: Option<Box<dyn Action>>,
211        handler: impl Fn(&mut Window, &mut App) + 'static,
212    ) -> Self {
213        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
214            toggle: None,
215            label: label.into(),
216            handler: Rc::new(move |_, window, cx| handler(window, cx)),
217            icon: None,
218            icon_position: IconPosition::End,
219            icon_size: IconSize::Small,
220            icon_color: None,
221            action,
222            disabled: false,
223            documentation_aside: None,
224        }));
225        self
226    }
227
228    pub fn toggleable_entry(
229        mut self,
230        label: impl Into<SharedString>,
231        toggled: bool,
232        position: IconPosition,
233        action: Option<Box<dyn Action>>,
234        handler: impl Fn(&mut Window, &mut App) + 'static,
235    ) -> Self {
236        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
237            toggle: Some((position, toggled)),
238            label: label.into(),
239            handler: Rc::new(move |_, window, cx| handler(window, cx)),
240            icon: None,
241            icon_position: position,
242            icon_size: IconSize::Small,
243            icon_color: None,
244            action,
245            disabled: false,
246            documentation_aside: None,
247        }));
248        self
249    }
250
251    pub fn custom_row(
252        mut self,
253        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
254    ) -> Self {
255        self.items.push(ContextMenuItem::CustomEntry {
256            entry_render: Box::new(entry_render),
257            handler: Rc::new(|_, _, _| {}),
258            selectable: false,
259        });
260        self
261    }
262
263    pub fn custom_entry(
264        mut self,
265        entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
266        handler: impl Fn(&mut Window, &mut App) + 'static,
267    ) -> Self {
268        self.items.push(ContextMenuItem::CustomEntry {
269            entry_render: Box::new(entry_render),
270            handler: Rc::new(move |_, window, cx| handler(window, cx)),
271            selectable: true,
272        });
273        self
274    }
275
276    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
277        self.items.push(ContextMenuItem::Label(label.into()));
278        self
279    }
280
281    pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
282        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
283            toggle: None,
284            label: label.into(),
285            action: Some(action.boxed_clone()),
286            handler: Rc::new(move |context, window, cx| {
287                if let Some(context) = &context {
288                    window.focus(context);
289                }
290                window.dispatch_action(action.boxed_clone(), cx);
291            }),
292            icon: None,
293            icon_position: IconPosition::End,
294            icon_size: IconSize::Small,
295            icon_color: None,
296            disabled: false,
297            documentation_aside: None,
298        }));
299        self
300    }
301
302    pub fn disabled_action(
303        mut self,
304        label: impl Into<SharedString>,
305        action: Box<dyn Action>,
306    ) -> Self {
307        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
308            toggle: None,
309            label: label.into(),
310            action: Some(action.boxed_clone()),
311            handler: Rc::new(move |context, window, cx| {
312                if let Some(context) = &context {
313                    window.focus(context);
314                }
315                window.dispatch_action(action.boxed_clone(), cx);
316            }),
317            icon: None,
318            icon_size: IconSize::Small,
319            icon_position: IconPosition::End,
320            icon_color: None,
321            disabled: true,
322            documentation_aside: None,
323        }));
324        self
325    }
326
327    pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
328        self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
329            toggle: None,
330            label: label.into(),
331            action: Some(action.boxed_clone()),
332            handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
333            icon: Some(IconName::ArrowUpRight),
334            icon_size: IconSize::XSmall,
335            icon_position: IconPosition::End,
336            icon_color: None,
337            disabled: false,
338            documentation_aside: None,
339        }));
340        self
341    }
342
343    pub fn keep_open_on_confirm(mut self) -> Self {
344        self.keep_open_on_confirm = true;
345        self
346    }
347
348    pub fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
349        let context = self.action_context.as_ref();
350        if let Some(
351            ContextMenuItem::Entry(ContextMenuEntry {
352                handler,
353                disabled: false,
354                ..
355            })
356            | ContextMenuItem::CustomEntry { handler, .. },
357        ) = self.selected_index.and_then(|ix| self.items.get(ix))
358        {
359            (handler)(context, window, cx)
360        }
361
362        if !self.keep_open_on_confirm {
363            cx.emit(DismissEvent);
364        }
365    }
366
367    pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
368        cx.emit(DismissEvent);
369        cx.emit(DismissEvent);
370    }
371
372    fn select_first(&mut self, _: &SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
373        if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) {
374            self.select_index(ix);
375        }
376        cx.notify();
377    }
378
379    pub fn select_last(&mut self) -> Option<usize> {
380        for (ix, item) in self.items.iter().enumerate().rev() {
381            if item.is_selectable() {
382                return self.select_index(ix);
383            }
384        }
385        None
386    }
387
388    fn handle_select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context<Self>) {
389        if self.select_last().is_some() {
390            cx.notify();
391        }
392    }
393
394    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
395        if let Some(ix) = self.selected_index {
396            let next_index = ix + 1;
397            if self.items.len() <= next_index {
398                self.select_first(&SelectFirst, window, cx);
399            } else {
400                for (ix, item) in self.items.iter().enumerate().skip(next_index) {
401                    if item.is_selectable() {
402                        self.select_index(ix);
403                        cx.notify();
404                        break;
405                    }
406                }
407            }
408        } else {
409            self.select_first(&SelectFirst, window, cx);
410        }
411    }
412
413    pub fn select_previous(
414        &mut self,
415        _: &SelectPrevious,
416        window: &mut Window,
417        cx: &mut Context<Self>,
418    ) {
419        if let Some(ix) = self.selected_index {
420            if ix == 0 {
421                self.handle_select_last(&SelectLast, window, cx);
422            } else {
423                for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
424                    if item.is_selectable() {
425                        self.select_index(ix);
426                        cx.notify();
427                        break;
428                    }
429                }
430            }
431        } else {
432            self.handle_select_last(&SelectLast, window, cx);
433        }
434    }
435
436    fn select_index(&mut self, ix: usize) -> Option<usize> {
437        self.documentation_aside = None;
438        let item = self.items.get(ix)?;
439        if item.is_selectable() {
440            self.selected_index = Some(ix);
441            if let ContextMenuItem::Entry(entry) = item {
442                if let Some(callback) = &entry.documentation_aside {
443                    self.documentation_aside = Some((ix, callback.clone()));
444                }
445            }
446        }
447        Some(ix)
448    }
449
450    pub fn on_action_dispatch(
451        &mut self,
452        dispatched: &dyn Action,
453        window: &mut Window,
454        cx: &mut Context<Self>,
455    ) {
456        if self.clicked {
457            cx.propagate();
458            return;
459        }
460
461        if let Some(ix) = self.items.iter().position(|item| {
462            if let ContextMenuItem::Entry(ContextMenuEntry {
463                action: Some(action),
464                disabled: false,
465                ..
466            }) = item
467            {
468                action.partial_eq(dispatched)
469            } else {
470                false
471            }
472        }) {
473            self.select_index(ix);
474            self.delayed = true;
475            cx.notify();
476            let action = dispatched.boxed_clone();
477            cx.spawn_in(window, |this, mut cx| async move {
478                cx.background_executor()
479                    .timer(Duration::from_millis(50))
480                    .await;
481                cx.update(|window, cx| {
482                    this.update(cx, |this, cx| {
483                        this.cancel(&menu::Cancel, window, cx);
484                        window.dispatch_action(action, cx);
485                    })
486                })
487            })
488            .detach_and_log_err(cx);
489        } else {
490            cx.propagate()
491        }
492    }
493
494    pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self {
495        self._on_blur_subscription = new_subscription;
496        self
497    }
498
499    fn render_menu_item(
500        &self,
501        ix: usize,
502        item: &ContextMenuItem,
503        window: &mut Window,
504        cx: &mut Context<Self>,
505    ) -> impl IntoElement {
506        match item {
507            ContextMenuItem::Separator => ListSeparator.into_any_element(),
508            ContextMenuItem::Header(header) => ListSubHeader::new(header.clone())
509                .inset(true)
510                .into_any_element(),
511            ContextMenuItem::Label(label) => ListItem::new(ix)
512                .inset(true)
513                .disabled(true)
514                .child(Label::new(label.clone()))
515                .into_any_element(),
516            ContextMenuItem::Entry(entry) => self
517                .render_menu_entry(ix, entry, window, cx)
518                .into_any_element(),
519            ContextMenuItem::CustomEntry {
520                entry_render,
521                handler,
522                selectable,
523            } => {
524                let handler = handler.clone();
525                let menu = cx.entity().downgrade();
526                let selectable = *selectable;
527                ListItem::new(ix)
528                    .inset(true)
529                    .toggle_state(if selectable {
530                        Some(ix) == self.selected_index
531                    } else {
532                        false
533                    })
534                    .selectable(selectable)
535                    .when(selectable, |item| {
536                        item.on_click({
537                            let context = self.action_context.clone();
538                            move |_, window, cx| {
539                                handler(context.as_ref(), window, cx);
540                                menu.update(cx, |menu, cx| {
541                                    menu.clicked = true;
542                                    cx.emit(DismissEvent);
543                                })
544                                .ok();
545                            }
546                        })
547                    })
548                    .child(entry_render(window, cx))
549                    .into_any_element()
550            }
551        }
552    }
553
554    fn render_menu_entry(
555        &self,
556        ix: usize,
557        entry: &ContextMenuEntry,
558        window: &mut Window,
559        cx: &mut Context<Self>,
560    ) -> impl IntoElement {
561        let ContextMenuEntry {
562            toggle,
563            label,
564            handler,
565            icon,
566            icon_position,
567            icon_size,
568            icon_color,
569            action,
570            disabled,
571            documentation_aside,
572        } = entry;
573
574        let handler = handler.clone();
575        let menu = cx.entity().downgrade();
576
577        let icon_color = if *disabled {
578            Color::Muted
579        } else if toggle.is_some() {
580            icon_color.unwrap_or(Color::Accent)
581        } else {
582            icon_color.unwrap_or(Color::Default)
583        };
584
585        let label_color = if *disabled {
586            Color::Disabled
587        } else {
588            Color::Default
589        };
590
591        let label_element = if let Some(icon_name) = icon {
592            h_flex()
593                .gap_1p5()
594                .when(
595                    *icon_position == IconPosition::Start && toggle.is_none(),
596                    |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)),
597                )
598                .child(Label::new(label.clone()).color(label_color))
599                .when(*icon_position == IconPosition::End, |flex| {
600                    flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color))
601                })
602                .into_any_element()
603        } else {
604            Label::new(label.clone())
605                .color(label_color)
606                .into_any_element()
607        };
608
609        let documentation_aside_callback = documentation_aside.clone();
610
611        div()
612            .id(("context-menu-child", ix))
613            .when_some(
614                documentation_aside_callback.clone(),
615                |this, documentation_aside_callback| {
616                    this.occlude()
617                        .on_hover(cx.listener(move |menu, hovered, _, cx| {
618                            if *hovered {
619                                menu.documentation_aside =
620                                    Some((ix, documentation_aside_callback.clone()));
621                                cx.notify();
622                            } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix)
623                            {
624                                menu.documentation_aside = None;
625                                cx.notify();
626                            }
627                        }))
628                },
629            )
630            .child(
631                ListItem::new(ix)
632                    .inset(true)
633                    .disabled(*disabled)
634                    .toggle_state(Some(ix) == self.selected_index)
635                    .when_some(*toggle, |list_item, (position, toggled)| {
636                        let contents = div()
637                            .flex_none()
638                            .child(
639                                Icon::new(icon.unwrap_or(IconName::Check))
640                                    .color(icon_color)
641                                    .size(*icon_size),
642                            )
643                            .when(!toggled, |contents| contents.invisible());
644
645                        match position {
646                            IconPosition::Start => list_item.start_slot(contents),
647                            IconPosition::End => list_item.end_slot(contents),
648                        }
649                    })
650                    .child(
651                        h_flex()
652                            .w_full()
653                            .justify_between()
654                            .child(label_element)
655                            .debug_selector(|| format!("MENU_ITEM-{}", label))
656                            .children(action.as_ref().and_then(|action| {
657                                self.action_context
658                                    .as_ref()
659                                    .map(|focus| {
660                                        KeyBinding::for_action_in(&**action, focus, window, cx)
661                                    })
662                                    .unwrap_or_else(|| {
663                                        KeyBinding::for_action(&**action, window, cx)
664                                    })
665                                    .map(|binding| {
666                                        div().ml_4().child(binding).when(
667                                            *disabled && documentation_aside_callback.is_some(),
668                                            |parent| parent.invisible(),
669                                        )
670                                    })
671                            }))
672                            .when(
673                                *disabled && documentation_aside_callback.is_some(),
674                                |parent| {
675                                    parent.child(
676                                        Icon::new(IconName::Info)
677                                            .size(IconSize::XSmall)
678                                            .color(Color::Muted),
679                                    )
680                                },
681                            ),
682                    )
683                    .on_click({
684                        let context = self.action_context.clone();
685                        move |_, window, cx| {
686                            handler(context.as_ref(), window, cx);
687                            menu.update(cx, |menu, cx| {
688                                menu.clicked = true;
689                                cx.emit(DismissEvent);
690                            })
691                            .ok();
692                        }
693                    }),
694            )
695            .into_any_element()
696    }
697}
698
699impl ContextMenuItem {
700    fn is_selectable(&self) -> bool {
701        match self {
702            ContextMenuItem::Header(_)
703            | ContextMenuItem::Separator
704            | ContextMenuItem::Label { .. } => false,
705            ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
706            ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
707        }
708    }
709}
710
711impl Render for ContextMenu {
712    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
713        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
714        let window_size = window.viewport_size();
715        let rem_size = window.rem_size();
716        let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
717
718        let aside = self
719            .documentation_aside
720            .as_ref()
721            .map(|(_, callback)| callback.clone());
722
723        h_flex()
724            .when(is_wide_window, |this| this.flex_row())
725            .when(!is_wide_window, |this| this.flex_col())
726            .w_full()
727            .items_start()
728            .gap_1()
729            .child(div().children(aside.map(|aside| {
730                WithRemSize::new(ui_font_size)
731                    .occlude()
732                    .elevation_2(cx)
733                    .p_2()
734                    .overflow_hidden()
735                    .when(is_wide_window, |this| this.max_w_96())
736                    .when(!is_wide_window, |this| this.max_w_48())
737                    .child(aside(cx))
738            })))
739            .child(
740                WithRemSize::new(ui_font_size)
741                    .occlude()
742                    .elevation_2(cx)
743                    .flex()
744                    .flex_row()
745                    .child(
746                        v_flex()
747                            .id("context-menu")
748                            .min_w(px(200.))
749                            .max_h(vh(0.75, window))
750                            .flex_1()
751                            .overflow_y_scroll()
752                            .track_focus(&self.focus_handle(cx))
753                            .on_mouse_down_out(cx.listener(|this, _, window, cx| {
754                                this.cancel(&menu::Cancel, window, cx)
755                            }))
756                            .key_context("menu")
757                            .on_action(cx.listener(ContextMenu::select_first))
758                            .on_action(cx.listener(ContextMenu::handle_select_last))
759                            .on_action(cx.listener(ContextMenu::select_next))
760                            .on_action(cx.listener(ContextMenu::select_previous))
761                            .on_action(cx.listener(ContextMenu::confirm))
762                            .on_action(cx.listener(ContextMenu::cancel))
763                            .when(!self.delayed, |mut el| {
764                                for item in self.items.iter() {
765                                    if let ContextMenuItem::Entry(ContextMenuEntry {
766                                        action: Some(action),
767                                        disabled: false,
768                                        ..
769                                    }) = item
770                                    {
771                                        el = el.on_boxed_action(
772                                            &**action,
773                                            cx.listener(ContextMenu::on_action_dispatch),
774                                        );
775                                    }
776                                }
777                                el
778                            })
779                            .child(
780                                List::new().children(
781                                    self.items.iter().enumerate().map(|(ix, item)| {
782                                        self.render_menu_item(ix, item, window, cx)
783                                    }),
784                                ),
785                            ),
786                    ),
787            )
788    }
789}