use core::time::Duration;
use std::rc::Rc;
use fluidattacks_designs_domain::alert::{
colors, dialog_body, message_weight, Colors, IconPosition, Time, Variant, ACKNOWLEDGE_TEXT,
BORDER_WIDTH, CONTENT_GAP, CONTENT_PADDING_RIGHT, DISMISS_GLYPH, DISMISS_HOVER_ALPHA,
DISMISS_PADDING, DISMISS_SIZE, DISMISS_STYLE, GLYPH_GAP, GLYPH_SIZE, GLYPH_STYLE, LINE_HEIGHT,
MIN_WIDTH, PADDING, RADIUS, TEXT_SIZE, TITLE_WEIGHT,
};
use fluidattacks_designs_domain::typography::{Weight, FONT_PRIMARY};
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, rgb, AlignSelf, App, Div, Entity, EntityId, FontWeight, InteractiveElement as _,
IntoElement, ParentElement as _, RenderOnce, Rgba, SharedString,
StatefulInteractiveElement as _, Styled as _, Window,
};
use crate::icon::Icon;
use crate::modal::Modal;
use crate::theme::ActiveTheme as _;
type Handler = Rc<dyn Fn(&mut Window, &mut App)>;
fn font_weight(weight: Weight) -> FontWeight {
FontWeight(f32::from(weight.value()))
}
fn part(id: &SharedString, name: &str) -> SharedString {
SharedString::from(format!("{id}-{name}"))
}
struct Standing {
shown: bool,
armed: bool,
}
impl Standing {
const fn new() -> Self {
Self {
shown: true,
armed: false,
}
}
}
struct Timeout {
seconds: u64,
auto_hide: bool,
handler: Option<Handler>,
}
fn arm(state: &Entity<Standing>, timeout: Timeout, view: EntityId, window: &Window, cx: &mut App) {
state.update(cx, |standing, _| standing.armed = true);
let state = state.clone();
window
.spawn(cx, async move |task_cx| {
task_cx
.background_executor()
.timer(Duration::from_secs(timeout.seconds))
.await;
task_cx
.update(|task_window, app| {
if timeout.auto_hide {
state.update(app, |standing, _| standing.shown = false);
}
if let Some(handler) = &timeout.handler {
handler(task_window, app);
}
app.notify(view);
})
.ok();
})
.detach();
}
fn aligned(el: Div, position: IconPosition) -> Div {
match position {
IconPosition::Center => el.items_center(),
IconPosition::Top => el.items_start(),
}
}
fn wrapping(text: SharedString) -> Div {
div().flex_1().min_w_0().child(text)
}
fn line(glyph: Option<Div>, text: SharedString, weight: Weight, position: IconPosition) -> Div {
aligned(div().flex(), position)
.w_full()
.pr(px(CONTENT_PADDING_RIGHT))
.font_weight(font_weight(weight))
.children(glyph)
.child(wrapping(text))
}
fn dismiss_control(id: SharedString, ink: u32, press: Handler) -> impl IntoElement {
let wash = Rgba {
alpha: DISMISS_HOVER_ALPHA,
..rgb(ink)
};
let mut control = div();
control.style().align_self = Some(AlignSelf::FlexStart);
control
.id(id)
.flex()
.flex_none()
.p(px(DISMISS_PADDING))
.rounded(px(RADIUS))
.cursor_pointer()
.hover(move |style| style.bg(wash))
.on_click(move |_, window, app| {
app.stop_propagation();
press(window, app);
})
.child(
Icon::new(DISMISS_GLYPH)
.style(DISMISS_STYLE)
.size(DISMISS_SIZE)
.color(ink),
)
}
#[derive(IntoElement)]
pub struct Alert {
id: SharedString,
message: SharedString,
title: Option<SharedString>,
variant: Variant,
glyph: Option<SharedString>,
glyph_enabled: bool,
icon_position: IconPosition,
closable: bool,
auto_hide: bool,
time: Time,
on_close: Option<Handler>,
on_timeout: Option<Handler>,
}
impl Alert {
pub fn new(id: impl Into<SharedString>, message: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
message: message.into(),
title: None,
variant: Variant::default(),
glyph: None,
glyph_enabled: true,
icon_position: IconPosition::default(),
closable: false,
auto_hide: false,
time: Time::default(),
on_close: None,
on_timeout: None,
}
}
#[must_use]
pub const fn variant(mut self, variant: Variant) -> Self {
self.variant = variant;
self
}
#[must_use]
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn glyph(mut self, name: impl Into<SharedString>) -> Self {
self.glyph = Some(name.into());
self
}
#[must_use]
pub const fn glyph_enabled(mut self, enabled: bool) -> Self {
self.glyph_enabled = enabled;
self
}
#[must_use]
pub const fn icon_position(mut self, position: IconPosition) -> Self {
self.icon_position = position;
self
}
#[must_use]
pub const fn closable(mut self, closable: bool) -> Self {
self.closable = closable;
self
}
#[must_use]
pub const fn auto_hide(mut self, auto_hide: bool) -> Self {
self.auto_hide = auto_hide;
self
}
#[must_use]
pub const fn time(mut self, time: Time) -> Self {
self.time = time;
self
}
#[must_use]
pub fn on_close(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_close = Some(Rc::new(handler));
self
}
#[must_use]
pub fn on_timeout(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_timeout = Some(Rc::new(handler));
self
}
fn leading_glyph(&self, ink: u32) -> Option<Div> {
if !self.glyph_enabled {
return None;
}
let name = self
.glyph
.clone()
.unwrap_or_else(|| SharedString::from(self.variant.glyph()));
Some(
div()
.flex_none()
.mr(px(GLYPH_GAP))
.mt(px(self.icon_position.glyph_margin_top()))
.child(
Icon::new(name)
.style(GLYPH_STYLE)
.size(GLYPH_SIZE)
.color(ink),
),
)
}
fn closing(&self, state: &Entity<Standing>) -> Handler {
let state = state.clone();
let on_close = self.on_close.clone();
Rc::new(move |press_window: &mut Window, app: &mut App| {
state.update(app, |standing, _| standing.shown = false);
if let Some(handler) = &on_close {
handler(press_window, app);
}
})
}
fn banner(self, palette: Colors) -> Div {
let glyph = self.leading_glyph(palette.fg);
let position = self.icon_position;
let has_title = self.title.is_some();
let (title_glyph, message_glyph) = if has_title {
(glyph, None)
} else {
(None, glyph)
};
let column = div()
.flex()
.flex_col()
.flex_1()
.min_w_0()
.gap(px(CONTENT_GAP))
.when_some(self.title, |el, title| {
el.child(line(title_glyph, title, TITLE_WEIGHT, position))
})
.child(line(
message_glyph,
self.message,
message_weight(has_title),
position,
));
aligned(div().flex(), position)
.w_full()
.min_w(px(MIN_WIDTH))
.p(px(PADDING))
.rounded(px(RADIUS))
.border(px(BORDER_WIDTH))
.border_color(rgb(palette.border))
.bg(rgb(palette.bg))
.text_color(rgb(palette.fg))
.font_family(FONT_PRIMARY)
.text_size(px(TEXT_SIZE.px()))
.line_height(px(LINE_HEIGHT))
.child(column)
}
}
impl RenderOnce for Alert {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = window.use_keyed_state(self.id.clone(), cx, |_, _| Standing::new());
if !state.read(cx).armed {
let timeout = Timeout {
seconds: self.time.seconds(),
auto_hide: self.auto_hide,
handler: self.on_timeout.clone(),
};
arm(&state, timeout, window.current_view(), window, cx);
}
if !state.read(cx).shown {
return div();
}
let palette = colors(self.variant, &cx.theme().colors);
let dismiss = self
.closable
.then(|| dismiss_control(part(&self.id, "dismiss"), palette.fg, self.closing(&state)));
self.banner(palette).children(dismiss)
}
}
#[derive(IntoElement)]
pub struct AlertDialog {
id: SharedString,
title: SharedString,
message: SharedString,
variant: Variant,
acknowledge: SharedString,
on_acknowledge: Option<Handler>,
}
impl AlertDialog {
pub fn new(
id: impl Into<SharedString>,
title: impl Into<SharedString>,
message: impl Into<SharedString>,
) -> Self {
Self {
id: id.into(),
title: title.into(),
message: message.into(),
variant: Variant::default(),
acknowledge: SharedString::from(ACKNOWLEDGE_TEXT),
on_acknowledge: None,
}
}
#[must_use]
pub const fn variant(mut self, variant: Variant) -> Self {
self.variant = variant;
self
}
#[must_use]
pub fn acknowledge_text(mut self, text: impl Into<SharedString>) -> Self {
self.acknowledge = text.into();
self
}
#[must_use]
pub fn on_acknowledge(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_acknowledge = Some(Rc::new(handler));
self
}
}
impl RenderOnce for AlertDialog {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let ink = colors(self.variant, &cx.theme().colors).fg;
let glyph = SharedString::from(self.variant.glyph());
let acknowledged = self.on_acknowledge;
Modal::new(self.id)
.title(self.title)
.confirm_text(self.acknowledge)
.body_height(dialog_body())
.on_outcome(move |_outcome, window, app| {
if let Some(handler) = &acknowledged {
handler(window, app);
}
})
.child(
div()
.flex()
.items_start()
.gap(px(GLYPH_GAP))
.child(
div().flex_none().child(
Icon::new(glyph)
.style(GLYPH_STYLE)
.size(GLYPH_SIZE)
.color(ink),
),
)
.child(wrapping(self.message)),
)
}
}
#[cfg(test)]
mod tests {
use gpui::{px, relative, SharedString, Styled as _};
use super::wrapping;
#[test]
fn the_text_box_takes_the_line_and_may_shrink_to_nothing() {
let mut boxed = wrapping(SharedString::from(
"The repositories folder is no longer where it was configured",
));
let style = boxed.style();
assert_eq!(style.min_size.width, Some(px(0.0).into()));
assert_eq!(style.flex_basis, Some(relative(0.0).into()));
assert_eq!(style.flex_grow, Some(1.0));
}
}