use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use fluidattacks_designs_domain::alert::colors;
use fluidattacks_designs_domain::image_view::{
viewer_body, viewer_box, Notice, BORDER_WIDTH, DEFAULT_SCRIM_ALPHA, GLYPH_GAP, GLYPH_SIZE,
GLYPH_STYLE, LINE_HEIGHT, PADDING, RADIUS, SCRIM, TEXT_SIZE, VIEWER_SIZE,
};
use fluidattacks_designs_domain::modal::Outcome;
use fluidattacks_designs_domain::theme::Colors;
use fluidattacks_designs_domain::typography::FONT_PRIMARY;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, img, px, rgb, AnyElement, App, Div, Image, ImageFormat, Img, InteractiveElement as _,
IntoElement, ObjectFit, ParentElement as _, RenderOnce, Rgba, SharedString, Stateful,
StatefulInteractiveElement as _, Styled, StyledImage as _, Window,
};
use crate::icon::Icon;
use crate::loading::Loading;
use crate::modal::Modal;
use crate::theme::ActiveTheme as _;
use crate::tooltip::Tooltip;
type Handler = Rc<dyn Fn(&mut Window, &mut App)>;
enum Source {
Bytes(Arc<Image>),
Path(Arc<Path>),
Video,
}
fn sized<T: Styled + IntoElement>(element: T, (width, height): (Option<f32>, Option<f32>)) -> T {
element
.when_some(width, |el, value| el.w(px(value)))
.when_some(height, |el, value| el.h(px(value)))
}
fn notice_box(notice: Notice, theme: &Colors) -> Div {
let palette = colors(notice.variant(), theme);
div()
.flex()
.items_center()
.justify_center()
.gap(px(GLYPH_GAP))
.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(
div().flex_none().child(
Icon::new(SharedString::from(notice.variant().glyph()))
.style(GLYPH_STYLE)
.size(GLYPH_SIZE)
.color(palette.fg),
),
)
.child(notice.text())
}
fn loading_box() -> Div {
div()
.flex()
.items_center()
.justify_center()
.child(Loading::new())
}
fn picture(element: Img, id: SharedString, theme: Colors) -> Stateful<Img> {
element
.id(id)
.object_fit(ObjectFit::Contain)
.with_loading(|| loading_box().into_any_element())
.with_fallback(move || notice_box(Notice::Failure, &theme).into_any_element())
}
fn body(
source: Option<Source>,
id: SharedString,
theme: Colors,
size: (Option<f32>, Option<f32>),
) -> AnyElement {
match source {
None => sized(loading_box(), size).into_any_element(),
Some(Source::Video) => sized(notice_box(Notice::Video, &theme), size).into_any_element(),
Some(Source::Bytes(image)) => {
sized(picture(img(image), id, theme), size).into_any_element()
}
Some(Source::Path(path)) => sized(picture(img(path), id, theme), size).into_any_element(),
}
}
fn frame(id: SharedString, inner: AnyElement, scrim: f32, press: Option<Handler>) -> Stateful<Div> {
let wash = Rgba {
alpha: scrim,
..rgb(SCRIM)
};
div()
.id(id)
.relative()
.when_some(press, |el, handler| {
el.cursor_pointer().on_click(move |_event, window, app| {
app.stop_propagation();
handler(window, app);
})
})
.child(inner)
.when(scrim > DEFAULT_SCRIM_ALPHA, |el| {
el.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.bg(wash),
)
})
}
#[derive(IntoElement)]
pub struct ImageView {
id: SharedString,
source: Option<Source>,
alt: Option<SharedString>,
width: Option<f32>,
height: Option<f32>,
scrim: f32,
on_click: Option<Handler>,
}
impl ImageView {
#[must_use]
pub fn new(id: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
source: None,
alt: None,
width: None,
height: None,
scrim: DEFAULT_SCRIM_ALPHA,
on_click: None,
}
}
#[must_use]
pub fn bytes(mut self, format: ImageFormat, bytes: impl Into<Vec<u8>>) -> Self {
self.source = Some(Source::Bytes(Arc::new(Image::from_bytes(
format,
bytes.into(),
))));
self
}
#[must_use]
pub fn path(mut self, path: impl Into<Arc<Path>>) -> Self {
self.source = Some(Source::Path(path.into()));
self
}
#[must_use]
pub fn video(mut self) -> Self {
self.source = Some(Source::Video);
self
}
#[must_use]
pub fn alt(mut self, alt: impl Into<SharedString>) -> Self {
self.alt = Some(alt.into());
self
}
#[must_use]
pub const fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
#[must_use]
pub const fn height(mut self, height: f32) -> Self {
self.height = Some(height);
self
}
#[must_use]
pub const fn scrim(mut self, alpha: f32) -> Self {
self.scrim = alpha;
self
}
#[must_use]
pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_click = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ImageView {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().colors;
let frame_id = SharedString::from(format!("{}-frame", self.id));
let tip_id = SharedString::from(format!("{}-alt", self.id));
let alt = self.alt.unwrap_or_default();
let undescribed = alt.is_empty();
let inner = body(self.source, self.id, theme, (self.width, self.height));
Tooltip::new(tip_id, alt).disabled(undescribed).child(frame(
frame_id,
inner,
self.scrim,
self.on_click,
))
}
}
#[derive(IntoElement)]
pub struct ImageViewer {
id: SharedString,
title: SharedString,
picture: ImageView,
on_close: Option<Handler>,
}
impl ImageViewer {
#[must_use]
pub fn new(
id: impl Into<SharedString>,
title: impl Into<SharedString>,
picture: ImageView,
) -> Self {
Self {
id: id.into(),
title: title.into(),
picture,
on_close: None,
}
}
#[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
}
}
impl RenderOnce for ImageViewer {
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
let viewport = window.viewport_size();
let (width, height) = viewer_box(f32::from(viewport.width), f32::from(viewport.height));
let closed = self.on_close;
Modal::new(self.id)
.title(self.title)
.size(VIEWER_SIZE)
.body_height(viewer_body(height))
.on_outcome(move |_outcome: &Outcome, outcome_window, app| {
if let Some(handler) = &closed {
handler(outcome_window, app);
}
})
.child(self.picture.width(width).height(height))
}
}