modal: Add scrollbar to Modal for overflow contents. (#960)

- Add `overlay` variable to Theme.

<img width="536" alt="image"
src="https://github.com/user-attachments/assets/12ed43c8-00b5-41b1-8cf6-0f31b533525c"
/>
This commit is contained in:
Jason Lee 2025-06-16 17:35:02 +08:00 committed by GitHub
parent 61283a71f0
commit d2633faf82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 89 additions and 50 deletions

View file

@ -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<Self>) -> 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"),
))
});
})),
),
),
)
}

View file

@ -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();

View file

@ -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<AnyElement>,
footer: Option<FooterFn>,
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,

View file

@ -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),
}
}