Skip to main content

gpui_component/dialog/
dialog.rs

1use gpui_base::TestSupportExt as _;
2use std::{rc::Rc, sync::LazyLock, time::Duration};
3
4use gpui::{
5    Animation, AnimationExt as _, AnyElement, App, BoxShadow, ClickEvent, Edges, FocusHandle, Hsla,
6    InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, SharedString,
7    StyleRefinement, Styled, Window, WindowControlArea, anchored, div, hsla, point,
8    prelude::FluentBuilder, px,
9};
10use gpui_base::{ElementExt as _, TextSelectionScopeId};
11use rust_i18n::t;
12
13use crate::{
14    ActiveTheme as _, IconName, Root, Sizable as _, StyledExt, TITLE_BAR_HEIGHT, WindowExt as _,
15    animation::cubic_bezier,
16    button::{Button, ButtonVariant, ButtonVariants as _},
17    dialog::{DialogContent, DialogTitle},
18    scroll::ScrollableElement as _,
19    v_flex,
20};
21
22pub static ANIMATION_DURATION: LazyLock<Duration> = LazyLock::new(|| Duration::from_secs_f64(0.25));
23pub use gpui_base::actions::{Cancel, Confirm};
24
25/// Dialog button props.
26#[derive(Clone)]
27pub struct DialogButtonProps {
28    pub(crate) ok_text: Option<SharedString>,
29    pub(crate) ok_variant: ButtonVariant,
30    pub(crate) cancel_text: Option<SharedString>,
31    pub(crate) cancel_variant: ButtonVariant,
32    pub(crate) show_cancel: bool,
33    pub(crate) on_ok: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>,
34    pub(crate) on_cancel: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>,
35    pub(crate) on_close: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
36}
37
38impl Default for DialogButtonProps {
39    fn default() -> Self {
40        Self {
41            ok_text: None,
42            ok_variant: ButtonVariant::Primary,
43            cancel_text: None,
44            cancel_variant: ButtonVariant::default(),
45            show_cancel: false,
46            on_ok: Rc::new(|_, _, _| true),
47            on_cancel: Rc::new(|_, _, _| true),
48            on_close: Rc::new(|_, _, _| {}),
49        }
50    }
51}
52
53impl DialogButtonProps {
54    /// Sets the text of the OK button. Default is `OK`.
55    pub fn ok_text(mut self, ok_text: impl Into<SharedString>) -> Self {
56        self.ok_text = Some(ok_text.into());
57        self
58    }
59
60    /// Sets the variant of the OK button. Default is `ButtonVariant::Primary`.
61    pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self {
62        self.ok_variant = ok_variant;
63        self
64    }
65
66    /// Sets the text of the Cancel button. Default is `Cancel`.
67    pub fn cancel_text(mut self, cancel_text: impl Into<SharedString>) -> Self {
68        self.cancel_text = Some(cancel_text.into());
69        self
70    }
71
72    /// Sets the variant of the Cancel button. Default is `ButtonVariant::default()`.
73    pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self {
74        self.cancel_variant = cancel_variant;
75        self
76    }
77
78    /// Sets whether to show the Cancel button. Default is `false`.
79    pub fn show_cancel(mut self, show_cancel: bool) -> Self {
80        self.show_cancel = show_cancel;
81        self
82    }
83
84    /// Sets the callback for when the dialog is has been confirmed.
85    ///
86    /// The callback should return `true` to close the dialog, if return `false` the dialog will not be closed.
87    pub fn on_ok(
88        mut self,
89        on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
90    ) -> Self {
91        self.on_ok = Rc::new(on_ok);
92        self
93    }
94
95    /// Sets the callback for when the dialog is has been canceled.
96    ///
97    /// The callback should return `true` to close the dialog, if return `false` the dialog will not be closed.
98    pub fn on_cancel(
99        mut self,
100        on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
101    ) -> Self {
102        self.on_cancel = Rc::new(on_cancel);
103        self
104    }
105
106    pub(crate) fn render_ok(&self, _: &mut Window, _: &mut App) -> AnyElement {
107        let ok_text = self
108            .ok_text
109            .clone()
110            .unwrap_or_else(|| t!("Dialog.ok").into());
111        let ok_variant = self.ok_variant;
112
113        Button::new("ok")
114            .label(ok_text)
115            .with_variant(ok_variant)
116            .on_click(|_, window, cx| {
117                window.dispatch_action(Box::new(Confirm { secondary: false }), cx)
118            })
119            .into_any_element()
120    }
121
122    pub(crate) fn render_cancel(&self, _: &mut Window, _: &mut App) -> AnyElement {
123        let cancel_text = self
124            .cancel_text
125            .clone()
126            .unwrap_or_else(|| t!("Dialog.cancel").into());
127        let cancel_variant = self.cancel_variant;
128
129        Button::new("cancel")
130            .label(cancel_text)
131            .with_variant(cancel_variant)
132            .on_click(|_, window, cx| window.dispatch_action(Box::new(Cancel), cx))
133            .into_any_element()
134    }
135}
136
137type ContentBuilderFn = Rc<dyn Fn(DialogContent, &mut Window, &mut App) -> DialogContent + 'static>;
138
139#[derive(Clone)]
140pub(crate) struct DialogProps {
141    width: Pixels,
142    max_width: Option<Pixels>,
143    margin_top: Option<Pixels>,
144    close_button: bool,
145
146    overlay: bool,
147    overlay_closable: bool,
148    pub(crate) overlay_visible: bool,
149    keyboard: bool,
150}
151
152impl Default for DialogProps {
153    fn default() -> Self {
154        Self {
155            margin_top: None,
156            width: px(448.),
157            max_width: None,
158            overlay: true,
159            keyboard: true,
160            overlay_visible: false,
161            close_button: true,
162            overlay_closable: true,
163        }
164    }
165}
166
167enum BaseDialogRoot {
168    Dialog(gpui_base::Dialog),
169    AlertDialog(gpui_base::AlertDialog),
170}
171
172macro_rules! map_base_root {
173    ($self:expr, $method:ident($($arg:expr),* $(,)?)) => {
174        match $self {
175            BaseDialogRoot::Dialog(root) => BaseDialogRoot::Dialog(root.$method($($arg),*)),
176            BaseDialogRoot::AlertDialog(root) => {
177                BaseDialogRoot::AlertDialog(root.$method($($arg),*))
178            }
179        }
180    };
181}
182
183impl BaseDialogRoot {
184    fn layer(self, index: usize, topmost: bool) -> Self {
185        map_base_root!(self, layer(index, topmost))
186    }
187    fn focus_handle(self, focus: FocusHandle) -> Self {
188        map_base_root!(self, focus_handle(focus))
189    }
190    fn close_on_escape(self, value: bool) -> Self {
191        map_base_root!(self, close_on_escape(value))
192    }
193    fn close_on_backdrop_press(self, value: bool) -> Self {
194        match self {
195            Self::Dialog(root) => Self::Dialog(root.close_on_backdrop_press(value)),
196            Self::AlertDialog(root) => Self::AlertDialog(root),
197        }
198    }
199    fn dismiss_below_y(self, value: Pixels) -> Self {
200        map_base_root!(self, dismiss_below_y(value))
201    }
202    fn backdrop(self, element: impl IntoElement) -> Self {
203        map_base_root!(self, backdrop(element))
204    }
205    fn popup(self, element: impl IntoElement) -> Self {
206        map_base_root!(self, popup(element))
207    }
208    fn on_ok(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static) -> Self {
209        map_base_root!(self, on_ok(handler))
210    }
211    fn on_cancel(
212        self,
213        handler: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
214    ) -> Self {
215        map_base_root!(self, on_cancel(handler))
216    }
217    fn on_close(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
218        map_base_root!(self, on_close(handler))
219    }
220    fn request_close(self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
221        map_base_root!(self, request_close(handler))
222    }
223}
224
225impl IntoElement for BaseDialogRoot {
226    type Element = <gpui_base::Dialog as IntoElement>::Element;
227    fn into_element(self) -> Self::Element {
228        match self {
229            Self::Dialog(root) => root.into_element(),
230            Self::AlertDialog(root) => root.into_element(),
231        }
232    }
233}
234
235/// A modal to display content in a dialog box.
236#[derive(IntoElement)]
237pub struct Dialog {
238    base: Option<BaseDialogRoot>,
239    pub(crate) style: StyleRefinement,
240    children: Vec<AnyElement>,
241    trigger: Option<AnyElement>,
242    title: Option<AnyElement>,
243    pub(crate) header: Option<AnyElement>,
244    pub(crate) footer: Option<AnyElement>,
245    pub(crate) content_builder: Option<ContentBuilderFn>,
246    pub(crate) props: DialogProps,
247
248    pub(super) button_props: DialogButtonProps,
249
250    /// This will be change when open the dialog, the focus handle is create when open the dialog.
251    pub(crate) focus_handle: FocusHandle,
252    pub(crate) layer_ix: usize,
253    pub(crate) selection_scope: TextSelectionScopeId,
254}
255
256pub(crate) fn overlay_color(overlay: bool, cx: &App) -> Hsla {
257    if !overlay {
258        return hsla(0., 0., 0., 0.);
259    }
260
261    cx.theme().overlay
262}
263
264impl Dialog {
265    /// Create a new dialog.
266    pub fn new(cx: &mut App) -> Self {
267        Self {
268            base: Some(BaseDialogRoot::Dialog(gpui_base::Dialog::new(cx))),
269            focus_handle: cx.focus_handle(),
270            style: StyleRefinement::default(),
271            trigger: None,
272            title: None,
273            header: None,
274            footer: None,
275            content_builder: None,
276            props: DialogProps::default(),
277            children: Vec::new(),
278            layer_ix: 0,
279            selection_scope: TextSelectionScopeId::default(),
280            button_props: DialogButtonProps::default(),
281        }
282    }
283
284    /// Sets the trigger element for the dialog.
285    ///
286    /// When a trigger is set, the dialog will render as a trigger button that opens the dialog when clicked.
287    pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
288        self.trigger = Some(trigger.into_any_element());
289        self
290    }
291
292    /// Sets the content of the dialog.
293    pub fn content<F>(mut self, builder: F) -> Self
294    where
295        F: Fn(DialogContent, &mut Window, &mut App) -> DialogContent + 'static,
296    {
297        self.content_builder = Some(Rc::new(builder));
298        self
299    }
300
301    /// Sets the title of the dialog.
302    pub fn title(mut self, title: impl IntoElement) -> Self {
303        self.title = Some(title.into_any_element());
304        self
305    }
306
307    /// Sets the footer of the dialog, the footer will render at the bottom of the dialog, usually for action buttons.
308    ///
309    /// When you set the footer, the `button_props` will be ignored, you need to render the action buttons by yourself.
310    pub(crate) fn header(mut self, header: impl IntoElement) -> Self {
311        self.header = Some(header.into_any_element());
312        self
313    }
314
315    /// Sets the footer of the dialog, the footer will render at the bottom of the dialog, usually for action buttons.
316    ///
317    /// When you set the footer, the `button_props` will be ignored, you need to render the action buttons by yourself.
318    pub fn footer(mut self, footer: impl IntoElement) -> Self {
319        self.footer = Some(footer.into_any_element());
320        self
321    }
322
323    /// Set the button props of the dialog.
324    pub fn button_props(mut self, button_props: DialogButtonProps) -> Self {
325        self.button_props = button_props;
326        self
327    }
328    pub(crate) fn with_base_alert_dialog(mut self, base: gpui_base::AlertDialog) -> Self {
329        self.base = Some(BaseDialogRoot::AlertDialog(base));
330        self.props.overlay_closable = false;
331        self
332    }
333
334    /// Sets the callback for when the dialog is closed.
335    ///
336    /// Called after [`Self::on_ok`] or [`Self::on_cancel`] callback.
337    pub fn on_close(
338        mut self,
339        on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
340    ) -> Self {
341        self.button_props.on_close = Rc::new(on_close);
342        self
343    }
344
345    /// Sets the callback for when the dialog is has been confirmed.
346    ///
347    /// The callback should return `true` to close the dialog, if return `false` the dialog will not be closed.
348    pub fn on_ok(
349        mut self,
350        on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
351    ) -> Self {
352        self.button_props = self.button_props.on_ok(on_ok);
353        self
354    }
355
356    /// Sets the callback for when the dialog is has been canceled.
357    ///
358    /// The callback should return `true` to close the dialog, if return `false` the dialog will not be closed.
359    pub fn on_cancel(
360        mut self,
361        on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
362    ) -> Self {
363        self.button_props = self.button_props.on_cancel(on_cancel);
364        self
365    }
366
367    /// Sets the false to hide close icon, default: true
368    pub fn close_button(mut self, close_button: bool) -> Self {
369        self.props.close_button = close_button;
370        self
371    }
372
373    /// Set the top offset of the dialog, defaults to None, will use the 1/10 of the viewport height.
374    pub fn margin_top(mut self, margin_top: impl Into<Pixels>) -> Self {
375        self.props.margin_top = Some(margin_top.into());
376        self
377    }
378
379    /// Sets the width of the dialog, defaults to 448px.
380    ///
381    /// See also [`Self::width`]
382    pub fn w(mut self, width: impl Into<Pixels>) -> Self {
383        self.props.width = width.into();
384        self
385    }
386
387    /// Sets the width of the dialog, defaults to 448px.
388    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
389        self.props.width = width.into();
390        self
391    }
392
393    /// Set the maximum width of the dialog, defaults to `None`.
394    pub fn max_w(mut self, max_width: impl Into<Pixels>) -> Self {
395        self.props.max_width = Some(max_width.into());
396        self
397    }
398
399    /// Set the overlay of the dialog, defaults to `true`.
400    pub fn overlay(mut self, overlay: bool) -> Self {
401        self.props.overlay = overlay;
402        self
403    }
404
405    /// Set the overlay closable of the dialog, defaults to `true`.
406    ///
407    /// When the overlay is clicked, the dialog will be closed.
408    pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
409        self.props.overlay_closable = overlay_closable;
410        self
411    }
412
413    /// Set whether to support keyboard esc to close the dialog, defaults to `true`.
414    pub fn keyboard(mut self, keyboard: bool) -> Self {
415        self.props.keyboard = keyboard;
416        self
417    }
418
419    pub(crate) fn has_overlay(&self) -> bool {
420        self.props.overlay
421    }
422
423    pub(crate) fn with_props(mut self, props: DialogProps) -> Self {
424        self.props = props;
425        self
426    }
427
428    fn defer_close_dialog(window: &mut Window, cx: &mut App) {
429        Root::update(window, cx, |root, window, cx| {
430            root.defer_close_dialog(window, cx);
431        });
432    }
433}
434
435impl ParentElement for Dialog {
436    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
437        self.children.extend(elements);
438    }
439}
440
441impl Styled for Dialog {
442    fn style(&mut self) -> &mut gpui::StyleRefinement {
443        &mut self.style
444    }
445}
446
447impl Dialog {
448    fn render_trigger(self, trigger: AnyElement, _: &mut Window, _: &mut App) -> AnyElement {
449        let content_builder = self.content_builder.clone();
450        let style = self.style.clone();
451        let props = self.props.clone();
452        let button_props = self.button_props.clone();
453
454        gpui_base::DialogTrigger::new(trigger)
455            .on_open(move |window, cx| {
456                let content_builder = content_builder.clone();
457                let style = style.clone();
458                let props = props.clone();
459                let button_props = button_props.clone();
460                window.open_dialog(cx, move |dialog, _, _| {
461                    dialog
462                        .refine_style(&style)
463                        .button_props(button_props.clone())
464                        .with_props(props.clone())
465                        .content({
466                            let content_builder = content_builder.clone();
467                            move |content, window, cx| {
468                                if let Some(builder) = content_builder.clone() {
469                                    builder(content, window, cx)
470                                } else {
471                                    content
472                                }
473                            }
474                        })
475                });
476            })
477            .into_any_element()
478    }
479}
480
481impl RenderOnce for Dialog {
482    fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
483        if let Some(trigger) = self.trigger.take() {
484            return self.render_trigger(trigger, window, cx);
485        }
486
487        let layer_ix = self.layer_ix;
488        let selection_scope = self.selection_scope;
489        let on_close = self.button_props.on_close.clone();
490        let on_ok = self.button_props.on_ok.clone();
491        let on_cancel = self.button_props.on_cancel.clone();
492
493        let window_paddings = crate::window_border::window_paddings(window);
494        let view_size = window.viewport_size()
495            - gpui::size(
496                window_paddings.left + window_paddings.right,
497                window_paddings.top + window_paddings.bottom,
498            );
499        let y = self.props.margin_top.unwrap_or(view_size.height / 10.) + px(layer_ix as f32 * 16.);
500        let x = view_size.width / 2. - self.props.width / 2.;
501
502        let base_size = window.text_style().font_size;
503        let rem_size = window.rem_size();
504
505        let mut paddings = Edges::all(px(16.));
506        if let Some(pl) = self.style.padding.left {
507            paddings.left = pl.to_pixels(base_size, rem_size);
508        }
509        if let Some(pr) = self.style.padding.right {
510            paddings.right = pr.to_pixels(base_size, rem_size);
511        }
512        if let Some(pt) = self.style.padding.top {
513            paddings.top = pt.to_pixels(base_size, rem_size);
514        }
515        if let Some(pb) = self.style.padding.bottom {
516            paddings.bottom = pb.to_pixels(base_size, rem_size);
517        }
518
519        // x1 = 1/3, x2 = 2/3 make the bezier's time mapping the identity,
520        // preserving the trajectory this dialog was tuned with before
521        // `cubic_bezier` solved for x; vaul's (0.32, 0.72, 0., 1.) is far
522        // more front-loaded under the CSS-correct solver.
523        let animation = Animation::new(*ANIMATION_DURATION).with_easing(cubic_bezier(
524            1. / 3.,
525            0.72,
526            2. / 3.,
527            1.,
528        ));
529
530        anchored()
531            .position(point(window_paddings.left, window_paddings.top))
532            .snap_to_window()
533            .child(
534                div()
535                    .id("dialog")
536                    .test_support()
537                    .occlude()
538                    .w(view_size.width)
539                    .h(view_size.height)
540                    .child(
541                        self.base
542                            .take()
543                            .expect("Dialog base host is always present")
544                            .layer(
545                                layer_ix,
546                                (self.layer_ix + 1) == Root::read(window, cx).active_dialogs.len(),
547                            )
548                            .focus_handle(self.focus_handle.clone())
549                            .close_on_escape(self.props.keyboard)
550                            .close_on_backdrop_press(self.props.overlay_closable)
551                            .dismiss_below_y(TITLE_BAR_HEIGHT)
552                            .when(self.props.overlay, |this| {
553                                this.backdrop(
554                                    div()
555                                        .absolute()
556                                        .size_full()
557                                        .window_control_area(WindowControlArea::Drag)
558                                        .when(self.props.overlay_visible, |overlay| {
559                                            overlay.bg(overlay_color(true, cx))
560                                        }),
561                                )
562                            })
563                            .on_ok(move |event, window, cx| on_ok(event, window, cx))
564                            .on_cancel(move |event, window, cx| on_cancel(event, window, cx))
565                            .on_close(move |event, window, cx| on_close(event, window, cx))
566                            .request_close(move |deferred, window, cx| {
567                                if deferred {
568                                    Self::defer_close_dialog(window, cx);
569                                } else {
570                                    window.close_dialog(cx);
571                                }
572                            })
573                            .popup(
574                                v_flex()
575                                    .id(layer_ix)
576                                    .test_support()
577                                    .bg(cx.theme().tokens.background)
578                                    .border_1()
579                                    .border_color(cx.theme().border)
580                                    .rounded(cx.theme().radius_lg)
581                                    .min_h_24()
582                                    .pt(paddings.top)
583                                    .pb(paddings.bottom)
584                                    .gap(paddings.top.max(px(8.)))
585                                    .refine_style(&self.style)
586                                    .px_0()
587                                    // There style is high priority, can't be overridden.
588                                    .absolute()
589                                    .occlude()
590                                    .relative()
591                                    .left(x)
592                                    .top(y)
593                                    .w(self.props.width)
594                                    .when_some(self.props.max_width, |this, w| this.max_w(w))
595                                    .child(
596                                        v_flex()
597                                            .flex_1()
598                                            .overflow_hidden()
599                                            .gap_y_2()
600                                            .when_some(self.header, |this, header| {
601                                                this.child(
602                                                    div()
603                                                        .pl(paddings.left)
604                                                        .pr(paddings.right)
605                                                        .child(header),
606                                                )
607                                            })
608                                            .when_some(self.title, |this, title| {
609                                                this.child(
610                                                    DialogTitle::new()
611                                                        .pl(paddings.left)
612                                                        .pr(paddings.right)
613                                                        .child(title),
614                                                )
615                                            })
616                                            .when_some(self.content_builder, |this, builder| {
617                                                this.child(builder(
618                                                    DialogContent::new()
619                                                        .gap(paddings.bottom)
620                                                        .pl(paddings.left)
621                                                        .pr(paddings.right),
622                                                    window,
623                                                    cx,
624                                                ))
625                                            })
626                                            .when(!self.children.is_empty(), |this| {
627                                                this.child(
628                                                    div().flex_1().overflow_hidden().child(
629                                                        // Body
630                                                        v_flex()
631                                                            .size_full()
632                                                            .overflow_y_scrollbar()
633                                                            .pl(paddings.left)
634                                                            .pr(paddings.right)
635                                                            .children(self.children),
636                                                    ),
637                                                )
638                                            }),
639                                    )
640                                    .when_some(self.footer, |this, footer| {
641                                        this.child(
642                                            div()
643                                                .pl(paddings.left)
644                                                .pr(paddings.right)
645                                                .child(footer),
646                                        )
647                                    })
648                                    .children(self.props.close_button.then(|| {
649                                        let top = (paddings.top - px(10.)).max(px(8.));
650                                        let right = (paddings.right - px(10.)).max(px(8.));
651
652                                        gpui_base::DialogClose::new()
653                                            .absolute()
654                                            .top(top)
655                                            .right(right)
656                                            .trigger(|button| {
657                                                Button::new("close")
658                                                    .with_base(button)
659                                                    .small()
660                                                    .ghost()
661                                                    .icon(IconName::Close)
662                                            })
663                                    }))
664                                    .with_animation(
665                                        "slide-down",
666                                        animation.clone(),
667                                        move |this, delta| {
668                                            // This is equivalent to `shadow_xl` with an extra opacity.
669                                            let shadow = vec![
670                                                BoxShadow {
671                                                    color: hsla(0., 0., 0., 0.1 * delta),
672                                                    offset: point(px(0.), px(20.)),
673                                                    blur_radius: px(25.),
674                                                    spread_radius: px(-5.),
675                                                    inset: false,
676                                                },
677                                                BoxShadow {
678                                                    color: hsla(0., 0., 0., 0.1 * delta),
679                                                    offset: point(px(0.), px(8.)),
680                                                    blur_radius: px(10.),
681                                                    spread_radius: px(-6.),
682                                                    inset: false,
683                                                },
684                                            ];
685                                            this.top(y * delta).shadow(shadow)
686                                        },
687                                    )
688                                    .text_selection_scope(selection_scope),
689                            ),
690                    )
691                    .with_animation("fade-in", animation, move |this, delta| this.opacity(delta)),
692            )
693            .into_any_element()
694    }
695}