1use super::*;
4use gpui::ClickEvent;
5use std::sync::Arc;
6
7const DRAG_SLOP: f64 = 2.0;
10
11#[derive(Default)]
13struct Shown(std::collections::HashMap<gpui::Resource, Arc<gpui::RenderImage>>);
14
15impl gpui::Global for Shown {}
16
17fn steady(source: ImageSource, window: &mut Window, cx: &mut App) -> ImageSource {
22 let ImageSource::Resource(resource) = &source else {
23 return source;
24 };
25 match window.use_asset::<gpui::ImgResourceLoader>(resource, cx) {
26 Some(Ok(image)) => {
27 cx.default_global::<Shown>()
28 .0
29 .insert(resource.clone(), image.clone());
30 ImageSource::Render(image)
31 }
32 _ => match cx.default_global::<Shown>().0.get(resource) {
33 Some(image) => ImageSource::Render(image.clone()),
34 None => source,
35 },
36 }
37}
38
39#[expect(
46 clippy::too_many_arguments,
47 reason = "a picture, its overlay, and what paints them"
48)]
49pub(super) fn image(
50 url: &str,
51 alt: &Text,
52 width: Option<u32>,
53 overlay: Overlay,
54 typography: &Typography,
55 theme: &Theme,
56 window: &mut Window,
57 cx: &mut App,
58) -> AnyElement {
59 let hint = SharedString::new_static(CAPTION_HINT);
60 let overlay = Overlay {
61 placeholder: Some(&hint),
62 ..overlay.at(Part::Caption)
63 };
64 let picture = if url.is_empty() {
65 div()
66 .cursor(CursorStyle::Arrow)
67 .h(px(IMAGE_EMPTY_HEIGHT))
68 .flex()
69 .items_center()
70 .px(px(CARD_PADDING))
71 .rounded(px(Theme::button_radius()))
72 .border_1()
73 .border_dashed()
74 .border_color(theme.border)
75 .text_size(px(typography.body.size()))
76 .text_color(theme.text_muted)
77 .child(IMAGE_EMPTY)
78 .into_any_element()
79 } else {
80 let picture = img(steady(image_source(url, overlay.base), window, cx));
81 let ix = overlay.block;
82 let box_ = div()
83 .id(ElementId::named_usize("md-picture", ix))
84 .cursor(CursorStyle::Arrow)
85 .relative()
86 .rounded(px(Theme::button_radius()))
87 .overflow_hidden()
88 .border_1()
89 .border_color(theme.border)
90 .when_some(overlay.image.cloned(), |el, on_image| {
91 el.on_click(move |event, window, cx| {
92 if let ClickEvent::Mouse(click) = event
93 && (click.up.position - click.down.position).magnitude() > DRAG_SLOP
94 {
95 return;
96 }
97 on_image(ix, window, cx);
98 })
99 })
100 .when_some(overlay.image_menu.cloned(), |el, on_menu| {
101 el.on_mouse_down(MouseButton::Right, move |event, window, cx| {
102 cx.stop_propagation();
103 on_menu(ix, event.position, window, cx);
104 })
105 })
106 .children(overlay.layouts.map(|layouts| {
107 let layouts = layouts.clone();
108 canvas(
109 move |bounds, _, _| layouts.record_picture(ix, bounds),
110 |_, _, _, _| (),
111 )
112 .absolute()
113 .size_full()
114 }));
115 let box_ = match width {
116 Some(width) => box_
121 .self_start()
122 .max_w_full()
123 .w(px(width as f32))
124 .child(picture.w(px(width as f32)).max_w_full()),
125 None => box_.self_start().max_w_full().child(picture.max_w_full()),
128 };
129 match overlay
130 .image_overlay
131 .and_then(|build| build(ix, url, window, cx))
132 {
133 Some(control) => {
134 let group = SharedString::from(format!("md-picture-overlay-{ix}"));
135 box_.group(group.clone())
136 .child(
137 div()
138 .id("image-overlay")
139 .absolute()
140 .map(|el| match overlay.image_overlay_corner {
141 ImageOverlayCorner::TopLeft => el.top(px(6.0)).left(px(6.0)),
142 ImageOverlayCorner::TopRight => el.top(px(6.0)).right(px(6.0)),
143 ImageOverlayCorner::BottomLeft => el.bottom(px(6.0)).left(px(6.0)),
144 ImageOverlayCorner::BottomRight => {
145 el.bottom(px(6.0)).right(px(6.0))
146 }
147 })
148 .invisible()
149 .group_hover(group, |style| style.visible())
150 .on_any_mouse_down(|_, _, cx| cx.stop_propagation())
151 .on_click(|_, _, cx| cx.stop_propagation())
152 .child(control),
153 )
154 .into_any_element()
155 }
156 None => box_.into_any_element(),
157 }
158 };
159 div()
160 .flex()
161 .flex_col()
162 .gap(px(CAPTION_GAP))
163 .child(picture)
164 .when(
167 overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
168 |el| {
169 el.child(text_element(
170 alt,
171 typography.caption.size(),
172 typography.caption.line_height(),
173 FontWeight::NORMAL,
174 overlay,
175 theme,
176 cx,
177 ))
178 },
179 )
180 .into_any_element()
181}
182
183pub(super) fn bookmark(
193 ix: usize,
194 url: &str,
195 base: Option<&Path>,
196 form: Form,
197 typography: &Typography,
198 theme: &Theme,
199 cx: &App,
200) -> AnyElement {
201 let preview = crate::app::link::preview(crate::Link::new(url, base), cx).unwrap_or_default();
202 let host = SharedString::from(preview::host(url).to_string());
203 let label = preview.label.clone().unwrap_or_else(|| host.clone());
204 let title = preview
205 .title
206 .clone()
207 .unwrap_or_else(|| SharedString::from(url.to_string()));
208
209 let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
212 let site = host.clone();
213 let mark = move |size: f32| {
214 let host = site.clone();
215 match icon.clone() {
216 Some(icon) => img(icon)
217 .size(px(size))
218 .rounded(px(size / 4.0))
219 .with_fallback(move || initial(&host, size, muted, wash))
220 .into_any_element(),
221 None => initial(&host, size, muted, wash),
222 }
223 };
224
225 let words = div()
226 .flex()
227 .flex_col()
228 .min_w_0()
229 .px(px(CARD_PADDING))
230 .py(px(CARD_PADDING - 2.0))
231 .child(
232 div()
233 .truncate()
234 .text_size(px(typography.body.size()))
235 .line_height(px(typography.body.line_height()))
236 .text_color(theme.text)
237 .child(title),
238 )
239 .children(preview.description.map(|blurb| {
240 div()
241 .line_clamp(2)
242 .text_size(px(typography.card.size()))
243 .line_height(px(typography.card.line_height()))
244 .text_color(theme.text_muted)
245 .child(blurb)
246 }))
247 .child(
248 div()
249 .mt_auto()
250 .pt(px(6.0))
251 .flex()
252 .items_center()
253 .gap(px(6.0))
254 .text_size(px(typography.card.size()))
255 .text_color(theme.text_muted)
256 .child(mark(CARD_ICON))
257 .child(div().truncate().child(label)),
258 );
259
260 let picture = corners(div(), form)
261 .bg(theme.surface)
262 .flex()
263 .items_center()
264 .justify_center()
265 .overflow_hidden()
266 .child(match preview.image {
267 Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
268 .with_fallback(move || mark(CARD_COVER))
269 .into_any_element(),
270 None => mark(CARD_COVER),
271 });
272
273 let open = url.to_string();
274 let base = base.map(Path::to_path_buf);
275 let card = div()
276 .id(ElementId::named_usize("md-bookmark", ix))
277 .flex()
278 .w_full()
279 .overflow_hidden()
280 .rounded(px(Theme::button_radius()))
281 .border(px(CARD_BORDER))
282 .border_color(theme.border)
283 .bg(theme.surface_card)
284 .cursor(CursorStyle::PointingHand)
285 .hover(|el| el.bg(theme::over(theme.element_hover, theme.surface_card)))
286 .on_click(move |_, window, cx| {
287 crate::app::link::open(crate::Link::new(&open, base.as_deref()), window, cx)
288 });
289
290 if matches!(form, Form::Embed(_)) {
291 card.flex_col()
292 .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
293 .child(words.w_full())
294 } else {
295 card.h(px(CARD_HEIGHT))
296 .child(words.flex_1())
297 .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
298 }
299 .into_any_element()
300}
301
302pub(super) fn corners<T: Styled>(element: T, form: Form) -> T {
306 let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
307 match form {
308 Form::Embed(_) => element.rounded_t(corner),
309 _ => element.rounded_r(corner),
310 }
311}
312
313pub(super) fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
316 div()
317 .flex_none()
318 .size(px(size))
319 .rounded(px(size / 4.0))
320 .bg(wash)
321 .flex()
322 .items_center()
323 .justify_center()
324 .text_size(px(size * 0.55))
325 .text_color(color)
326 .child(SharedString::from(
327 host.chars()
328 .next()
329 .unwrap_or('?')
330 .to_uppercase()
331 .to_string(),
332 ))
333 .into_any_element()
334}
335
336pub fn hover_bookmark(link: crate::Link<'_>, cx: &App) -> AnyElement {
338 let theme = Theme::of(cx).clone();
339 let card = bookmark(
340 0,
341 link.url,
342 link.base,
343 Form::Embed(None),
344 &cx.typography(),
345 &theme,
346 cx,
347 );
348 div()
349 .w(px(MENTION_CARD_WIDTH))
350 .child(card)
351 .into_any_element()
352}
353
354pub(super) struct HoverCard {
356 url: String,
357 base: Option<std::path::PathBuf>,
358}
359
360impl HoverCard {
361 pub(super) fn view(url: &str, base: Option<std::path::PathBuf>, cx: &mut App) -> gpui::AnyView {
362 cx.new(|_| Self {
363 url: url.to_string(),
364 base,
365 })
366 .into()
367 }
368}
369
370impl Render for HoverCard {
371 fn render(&mut self, window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement {
372 let link = crate::Link::new(&self.url, self.base.as_deref());
373 div().children(crate::app::link::hover(link, window, cx))
374 }
375}