From d2633faf824218f53e829b02fd9b70810aeef74a Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 16 Jun 2025 17:35:02 +0800 Subject: [PATCH] modal: Add scrollbar to Modal for overflow contents. (#960) - Add `overlay` variable to Theme. image --- crates/story/src/modal_story.rs | 43 +++++++++++++--- crates/ui/src/drawer.rs | 2 +- crates/ui/src/modal.rs | 90 +++++++++++++++++---------------- crates/ui/src/theme.rs | 4 ++ 4 files changed, 89 insertions(+), 50 deletions(-) diff --git a/crates/story/src/modal_story.rs b/crates/story/src/modal_story.rs index f7f8cec4..f946f9c8 100644 --- a/crates/story/src/modal_story.rs +++ b/crates/story/src/modal_story.rs @@ -12,6 +12,7 @@ use gpui_component::{ h_flex, input::{InputState, TextInput}, modal::ModalButtonProps, + text::TextView, v_flex, ContextModal as _, }; @@ -184,6 +185,9 @@ impl Focusable for ModalStory { impl Render for ModalStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let modal_overlay = self.modal_overlay; + let overlay_closable = self.overlay_closable; + div() .id("modal-story") .track_focus(&self.focus_handle) @@ -270,10 +274,12 @@ impl Render for ModalStory { Button::new("confirm-modal0") .primary() .label("Submit") - .on_click(cx.listener(|_, _, window, cx| { - window.open_modal(cx, |modal, _, _| { + .on_click(cx.listener(move |_, _, window, cx| { + window.open_modal(cx, move |modal, _, _| { modal .confirm() + .overlay(modal_overlay) + .overlay_closable(overlay_closable) .child("Are you sure to submit?") .on_ok(|_, window, cx| { window @@ -296,10 +302,14 @@ impl Render for ModalStory { Button::new("confirm-modal1") .danger() .label("Delete Item") - .on_click(cx.listener(|_, _, window, cx| { - window.open_modal(cx, |modal, _, _| { + .on_click(cx.listener(move |_, _, window, cx| { + window.open_modal(cx, move |modal, _, _| { modal + .rounded_lg() + .p_3() .confirm() + .overlay(modal_overlay) + .overlay_closable(overlay_closable) .child("Are you sure to delete this item?") .button_props( ModalButtonProps::default() @@ -330,10 +340,12 @@ impl Render for ModalStory { section("Alert Modal").child( Button::new("alert-modal") .label("Alert") - .on_click(cx.listener(|_, _, window, cx| { - window.open_modal(cx, |modal, _, _| { + .on_click(cx.listener(move |_, _, window, cx| { + window.open_modal(cx, move |modal, _, _| { modal .confirm() + .overlay(modal_overlay) + .overlay_closable(overlay_closable) .child("You are successfully logged in.") .alert() .on_close(|_, window, cx| { @@ -343,6 +355,25 @@ impl Render for ModalStory { }); })), ), + ) + .child( + section("Scrollable Modal").child( + Button::new("scrollable-modal") + .label("Scrollable Modal") + .on_click(cx.listener(move |_, _, window, cx| { + window.open_modal(cx, move |modal, _, _| { + modal + .h(px(450.)) + .overlay(modal_overlay) + .overlay_closable(overlay_closable) + .title("Modal with scrollbar") + .child(TextView::markdown( + "markdown1", + include_str!("../../../README.md"), + )) + }); + })), + ), ), ) } diff --git a/crates/ui/src/drawer.rs b/crates/ui/src/drawer.rs index 4d809781..ed42aaee 100644 --- a/crates/ui/src/drawer.rs +++ b/crates/ui/src/drawer.rs @@ -136,7 +136,7 @@ impl RenderOnce for Drawer { .occlude() .w(size.width) .h(size.height - titlebar_height) - .bg(overlay_color(self.overlay, window, cx)) + .bg(overlay_color(self.overlay, cx)) .when(self.overlay, |this| { this.on_mouse_down(MouseButton::Left, { let on_close = self.on_close.clone(); diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index 96a22e3f..d7a50440 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -3,8 +3,8 @@ use std::{rc::Rc, time::Duration}; use gpui::{ anchored, div, hsla, point, prelude::FluentBuilder, px, relative, Animation, AnimationExt as _, AnyElement, App, Bounds, ClickEvent, Div, FocusHandle, Hsla, InteractiveElement, IntoElement, - KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, Styled, - Window, + KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, + StyleRefinement, Styled, Window, }; use rust_i18n::t; @@ -12,7 +12,9 @@ use crate::{ actions::{Cancel, Confirm}, animation::cubic_bezier, button::{Button, ButtonVariant, ButtonVariants as _}, - h_flex, v_flex, ActiveTheme as _, ContextModal, IconName, Root, Sizable as _, StyledExt, + h_flex, + scroll::ScrollbarAxis, + v_flex, ActiveTheme as _, ContextModal, IconName, Root, Sizable as _, StyledExt, }; const CONTEXT: &str = "Modal"; @@ -74,7 +76,7 @@ impl ModalButtonProps { #[derive(IntoElement)] pub struct Modal { - base: Div, + style: StyleRefinement, title: Option, footer: Option, content: Div, @@ -97,35 +99,19 @@ pub struct Modal { pub(crate) overlay_visible: bool, } -pub(crate) fn overlay_color(overlay: bool, _: &Window, cx: &App) -> Hsla { +pub(crate) fn overlay_color(overlay: bool, cx: &App) -> Hsla { if !overlay { return hsla(0., 0., 0., 0.); } - if cx.theme().mode.is_dark() { - hsla(0., 1., 1., 0.06) - } else { - hsla(0., 0., 0., 0.06) - } + cx.theme().overlay } impl Modal { pub fn new(_: &mut Window, cx: &mut App) -> Self { - let radius = (cx.theme().radius * 2.).min(px(20.)); - - let base = v_flex() - .bg(cx.theme().background) - .border_1() - .border_color(cx.theme().border) - .rounded(radius) - .shadow_xl() - .min_h_24() - .p_4() - .gap_4(); - Self { - base, focus_handle: cx.focus_handle(), + style: StyleRefinement::default(), title: None, footer: None, content: v_flex(), @@ -287,7 +273,7 @@ impl ParentElement for Modal { impl Styled for Modal { fn style(&mut self) -> &mut gpui::StyleRefinement { - self.base.style() + &mut self.style } } @@ -369,16 +355,18 @@ impl RenderOnce for Modal { let offset_top = px(layer_ix as f32 * 16.); let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top; let x = bounds.center().x - self.width / 2.; + let border_radius = (cx.theme().radius * 2.).min(px(20.)); anchored() .position(point(window_paddings.left, window_paddings.top)) .snap_to_window() .child( div() + .id("modal") .w(view_size.width) .h(view_size.height) .when(self.overlay_visible, |this| { - this.occlude().bg(overlay_color(self.overlay, window, cx)) + this.occlude().bg(overlay_color(self.overlay, cx)) }) .when(self.overlay_closable, |this| { // Only the last modal owns the `mouse down - close modal` event. @@ -397,8 +385,17 @@ impl RenderOnce for Modal { }) }) .child( - self.base - .id(SharedString::from(format!("modal-{layer_ix}"))) + v_flex() + .id(layer_ix) + .bg(cx.theme().background) + .border_1() + .border_color(cx.theme().border) + .rounded(border_radius) + .shadow_xl() + .min_h_24() + .py_4() + .gap_4() + .refine_style(&self.style) .key_context(CONTEXT) .track_focus(&self.focus_handle) .when(self.keyboard, |this| { @@ -440,34 +437,41 @@ impl RenderOnce for Modal { .when_some(self.max_width, |this, w| this.max_w(w)) .when_some(self.title, |this, title| { this.child( - div().font_semibold().line_height(relative(1.)).child(title), + div() + .font_semibold() + .px_4() + .line_height(relative(1.)) + .child(title), ) }) .when(self.show_close, |this| { this.child( - Button::new(SharedString::from(format!( - "modal-close-{layer_ix}" - ))) - .absolute() - .top_2() - .right_2() - .small() - .ghost() - .icon(IconName::Close) - .on_click( - move |_, window, cx| { + Button::new("close") + .absolute() + .top_2() + .right_2() + .small() + .ghost() + .icon(IconName::Close) + .on_click(move |_, window, cx| { on_cancel(&ClickEvent::default(), window, cx); on_close(&ClickEvent::default(), window, cx); window.close_modal(cx); - }, - ), + }), ) }) - .child(div().w_full().flex_1().child(self.content)) + .child( + div().w_full().flex_1().overflow_hidden().child( + v_flex() + .scrollable(window.current_view(), ScrollbarAxis::Vertical) + .px_4() + .child(self.content), + ), + ) .when(self.footer.is_some(), |this| { let footer = self.footer.unwrap(); - this.child(h_flex().gap_2().justify_end().children(footer( + this.child(h_flex().px_4().gap_2().justify_end().children(footer( render_ok, render_cancel, window, diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index af81b448..4811c2c8 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -243,6 +243,8 @@ pub struct ThemeColor { pub warning_hover: Hsla, /// Warning foreground color. pub warning_foreground: Hsla, + /// Overlay background color. + pub overlay: Hsla, /// Window border color. /// /// # Platform specific: @@ -346,6 +348,7 @@ impl ThemeColor { warning_active: crate::yellow_600(), warning_hover: crate::yellow_500().opacity(0.9), warning_foreground: crate::gray_50(), + overlay: hsl(0., 0., 0.).opacity(0.06), window_border: hsl(240.0, 5.9, 78.0), } } @@ -444,6 +447,7 @@ impl ThemeColor { warning_active: crate::yellow_800().darken(0.2), warning_foreground: crate::yellow_50(), warning_hover: crate::yellow_800().opacity(0.9), + overlay: hsl(0., 100., 100.).opacity(0.06), window_border: hsl(240.0, 3.7, 28.0), } }