use super::*;
use gpui::ClickEvent;
use std::sync::Arc;
const DRAG_SLOP: f64 = 2.0;
#[derive(Default)]
struct Shown(std::collections::HashMap<gpui::Resource, Arc<gpui::RenderImage>>);
impl gpui::Global for Shown {}
fn steady(source: ImageSource, window: &mut Window, cx: &mut App) -> ImageSource {
let ImageSource::Resource(resource) = &source else {
return source;
};
match window.use_asset::<gpui::ImgResourceLoader>(resource, cx) {
Some(Ok(image)) => {
cx.default_global::<Shown>()
.0
.insert(resource.clone(), image.clone());
ImageSource::Render(image)
}
_ => match cx.default_global::<Shown>().0.get(resource) {
Some(image) => ImageSource::Render(image.clone()),
None => source,
},
}
}
#[expect(
clippy::too_many_arguments,
reason = "a picture, its overlay, and what paints them"
)]
pub(super) fn image(
url: &str,
alt: &Text,
width: Option<u32>,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let hint = SharedString::new_static(CAPTION_HINT);
let overlay = Overlay {
placeholder: Some(&hint),
..overlay.at(Part::Caption)
};
let picture = if url.is_empty() {
div()
.cursor(CursorStyle::Arrow)
.h(px(IMAGE_EMPTY_HEIGHT))
.flex()
.items_center()
.px(px(CARD_PADDING))
.rounded(px(Theme::button_radius()))
.border_1()
.border_dashed()
.border_color(theme.border)
.text_size(px(typography.body.size()))
.text_color(theme.text_muted)
.child(IMAGE_EMPTY)
.into_any_element()
} else {
let picture = img(steady(image_source(url, overlay.base), window, cx));
let ix = overlay.block;
let box_ = div()
.id(ElementId::named_usize("md-picture", ix))
.cursor(CursorStyle::Arrow)
.relative()
.rounded(px(Theme::button_radius()))
.overflow_hidden()
.border_1()
.border_color(theme.border)
.when_some(overlay.image.cloned(), |el, on_image| {
el.on_click(move |event, window, cx| {
if let ClickEvent::Mouse(click) = event
&& (click.up.position - click.down.position).magnitude() > DRAG_SLOP
{
return;
}
on_image(ix, window, cx);
})
})
.when_some(overlay.image_menu.cloned(), |el, on_menu| {
el.on_mouse_down(MouseButton::Right, move |event, window, cx| {
cx.stop_propagation();
on_menu(ix, event.position, window, cx);
})
})
.children(overlay.layouts.map(|layouts| {
let layouts = layouts.clone();
canvas(
move |bounds, _, _| layouts.record_picture(ix, bounds),
|_, _, _, _| (),
)
.absolute()
.size_full()
}));
let box_ = match width {
Some(width) => box_
.self_start()
.max_w_full()
.w(px(width as f32))
.child(picture.w(px(width as f32)).max_w_full()),
None => box_.self_start().max_w_full().child(picture.max_w_full()),
};
match overlay
.image_overlay
.and_then(|build| build(ix, url, window, cx))
{
Some(control) => {
let group = SharedString::from(format!("md-picture-overlay-{ix}"));
box_.group(group.clone())
.child(
div()
.id("image-overlay")
.absolute()
.map(|el| match overlay.image_overlay_corner {
ImageOverlayCorner::TopLeft => el.top(px(6.0)).left(px(6.0)),
ImageOverlayCorner::TopRight => el.top(px(6.0)).right(px(6.0)),
ImageOverlayCorner::BottomLeft => el.bottom(px(6.0)).left(px(6.0)),
ImageOverlayCorner::BottomRight => {
el.bottom(px(6.0)).right(px(6.0))
}
})
.invisible()
.group_hover(group, |style| style.visible())
.on_any_mouse_down(|_, _, cx| cx.stop_propagation())
.on_click(|_, _, cx| cx.stop_propagation())
.child(control),
)
.into_any_element()
}
None => box_.into_any_element(),
}
};
div()
.flex()
.flex_col()
.gap(px(CAPTION_GAP))
.child(picture)
.when(
overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
|el| {
el.child(text_element(
alt,
typography.caption.size(),
typography.caption.line_height(),
FontWeight::NORMAL,
overlay,
theme,
cx,
))
},
)
.into_any_element()
}
pub(super) fn bookmark(
ix: usize,
url: &str,
base: Option<&Path>,
form: Form,
typography: &Typography,
theme: &Theme,
cx: &App,
) -> AnyElement {
Bookmark {
link: crate::Link::new(url, base),
form,
typography: *typography,
height: None,
}
.element(ix, theme, cx)
}
pub struct Bookmark<'a> {
pub link: crate::Link<'a>,
pub form: Form,
pub typography: Typography,
pub height: Option<Pixels>,
}
impl Bookmark<'_> {
pub fn render(self, cx: &App) -> AnyElement {
self.element(0, Theme::of(cx), cx)
}
fn element(self, ix: usize, theme: &Theme, cx: &App) -> AnyElement {
let Self {
link,
form,
typography,
height,
} = self;
let crate::Link { url, base, .. } = link;
let scale = typography.body.size() / cx.typography().body.size();
let preview =
crate::app::link::preview(crate::Link::new(url, base), cx).unwrap_or_default();
let host = SharedString::from(preview::host(url).to_string());
let label = preview.label.clone().unwrap_or_else(|| host.clone());
let title = preview
.title
.clone()
.unwrap_or_else(|| SharedString::from(url.to_string()));
let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
let site = host.clone();
let mark = move |size: f32| {
let host = site.clone();
match icon.clone() {
Some(icon) => img(icon)
.size(px(size))
.rounded(px(size / 4.0))
.with_fallback(move || initial(&host, size, muted, wash))
.into_any_element(),
None => initial(&host, size, muted, wash),
}
};
let padding = (CARD_PADDING - 2.0) * scale * 2.0 + CARD_BORDER * 2.0;
let footer = typography.card.line_height() + 6.0 * scale;
let title_lines = if height.is_some_and(|height| {
height.as_f32() < padding + footer + typography.body.line_height() * 2.0
}) {
1
} else {
2
};
let show_description = height.is_none_or(|height| {
height.as_f32()
>= padding
+ footer
+ typography.body.line_height() * title_lines as f32
+ typography.card.line_height() * 2.0
});
let words = div()
.flex()
.flex_col()
.min_w_0()
.px(px(CARD_PADDING * scale))
.py(px((CARD_PADDING - 2.0) * scale))
.child(
div()
.when(height.is_none(), |el| el.truncate())
.when(height.is_some(), |el| el.line_clamp(title_lines))
.text_size(px(typography.body.size()))
.line_height(px(typography.body.line_height()))
.text_color(theme.text)
.child(title),
)
.children(
preview
.description
.filter(|_| show_description)
.map(|blurb| {
div()
.line_clamp(2)
.text_size(px(typography.card.size()))
.line_height(px(typography.card.line_height()))
.text_color(theme.text_muted)
.child(blurb)
}),
)
.child(
div()
.mt_auto()
.flex_none()
.pt(px(6.0 * scale))
.flex()
.items_center()
.gap(px(6.0 * scale))
.text_size(px(typography.card.size()))
.text_color(theme.text_muted)
.child(mark(CARD_ICON * scale))
.child(div().truncate().child(label)),
);
let picture = corners(div(), form)
.bg(theme.surface)
.flex()
.items_center()
.justify_center()
.overflow_hidden()
.child(match preview.image {
Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
.with_fallback(move || mark(CARD_COVER * scale))
.into_any_element(),
None => mark(CARD_COVER * scale),
});
let open = url.to_string();
let base = base.map(Path::to_path_buf);
let card = div()
.id(ElementId::named_usize("md-bookmark", ix))
.flex()
.w_full()
.overflow_hidden()
.rounded(px(Theme::button_radius()))
.border(px(CARD_BORDER))
.border_color(theme.border)
.bg(theme.surface_card)
.cursor(CursorStyle::PointingHand)
.hover(|el| el.bg(theme::over(theme.element_hover, theme.surface_card)))
.on_click(move |_, window, cx| {
crate::app::link::open(crate::Link::new(&open, base.as_deref()), window, cx)
});
if matches!(form, Form::Embed(_)) {
card.flex_col()
.when_some(height, |el, height| el.h(height))
.child(
picture
.w_full()
.min_h_0()
.when(height.is_some(), |el| el.flex_1())
.when(height.is_none(), |el| el.h(px(CARD_COVER_HEIGHT * scale))),
)
.child(words.w_full().flex_none())
} else {
card.h(height.unwrap_or_else(|| px(CARD_HEIGHT * scale)))
.child(words.flex_1())
.child(picture.flex_none().w(px(CARD_IMAGE_WIDTH * scale)).h_full())
}
.into_any_element()
}
}
pub(super) fn corners<T: Styled>(element: T, form: Form) -> T {
let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
match form {
Form::Embed(_) => element.rounded_t(corner),
_ => element.rounded_r(corner),
}
}
pub(super) fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
div()
.flex_none()
.size(px(size))
.rounded(px(size / 4.0))
.bg(wash)
.flex()
.items_center()
.justify_center()
.text_size(px(size * 0.55))
.text_color(color)
.child(SharedString::from(
host.chars()
.next()
.unwrap_or('?')
.to_uppercase()
.to_string(),
))
.into_any_element()
}
pub fn hover_bookmark(link: crate::Link<'_>, cx: &App) -> AnyElement {
let theme = Theme::of(cx).clone();
let card = bookmark(
0,
link.url,
link.base,
Form::Embed(None),
&cx.typography(),
&theme,
cx,
);
div()
.w(px(MENTION_CARD_WIDTH))
.child(card)
.into_any_element()
}
pub(super) struct HoverCard {
url: String,
base: Option<std::path::PathBuf>,
}
impl HoverCard {
pub(super) fn view(url: &str, base: Option<std::path::PathBuf>, cx: &mut App) -> gpui::AnyView {
cx.new(|_| Self {
url: url.to_string(),
base,
})
.into()
}
}
impl Render for HoverCard {
fn render(&mut self, window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement {
let link = crate::Link::new(&self.url, self.base.as_deref());
div().children(crate::app::link::hover(link, window, cx))
}
}