use std::time::Duration; use gpui::{ Animation, AnimationExt, App, Div, ElementId, ImageSource, InteractiveElement, IntoElement, ObjectFit, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img, pulsating_between, }; use crate::{ primitives::{Icon, IconName}, theme::{ActiveTheme, IconSize}, }; const PULSE: Duration = Duration::from_millis(1600); /// Themed `img()`; a `Cover` crop keeps square corners. #[derive(IntoElement)] pub struct Image { id: ElementId, base: Div, source: Option, fit: ObjectFit, } impl Image { /// The id keeps gpui's loading state across frames. pub fn new(id: impl Into, source: impl Into) -> Self { Self { id: id.into(), base: div(), source: Some(source.into()), fit: ObjectFit::Cover, } } /// Nothing to load yet. pub fn placeholder(id: impl Into) -> Self { Self { id: id.into(), base: div(), source: None, fit: ObjectFit::Cover, } } pub fn fit(mut self, fit: ObjectFit) -> Self { self.fit = fit; self } } impl Styled for Image { fn style(&mut self) -> &mut StyleRefinement { self.base.style() } } impl RenderOnce for Image { fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let (sunken, subtle) = (theme.colors.sunken, theme.colors.fg_subtle); let still = theme.reduced_motion; let radii = self.base.style().corner_radii.clone(); let pane = { let radii = radii.clone(); move || { let mut pane = div().size_full().bg(sunken); pane.style().corner_radii = radii.clone(); pane } }; let loading_pane = pane.clone(); let mark = move |name| { pane() .flex() .items_center() .justify_center() .child(Icon::new(name).size(IconSize::Lg).color(subtle)) }; let Some(source) = self.source else { return self.base.overflow_hidden().child(mark(IconName::Image)); }; let mut picture = img(source) .id(self.id) .size_full() .object_fit(self.fit) .with_loading(move || { if still { return loading_pane().into_any_element(); } loading_pane() .with_animation( "image-loading", Animation::new(PULSE) .repeat() .with_easing(pulsating_between(0.55, 1.0)), |pane, t| pane.opacity(t), ) .into_any_element() }) .with_fallback(move || mark(IconName::ImageOff).into_any_element()); picture.style().corner_radii = radii; self.base.overflow_hidden().child(picture) } }