Skip to main content

markdown/app/
picture.rs

1//! What a picture's right-click menu offers, and what each row does.
2//!
3//! The rows and what each does, not the menu: whoever raises it — on
4//! [`crate::Editing::image_menu`], or by hit-testing
5//! [`crate::BlockLayouts::picture_bounds`] — holds it open and paints it.
6
7use std::path::{Path, PathBuf};
8
9use gpui::App;
10
11/// A picture as its right-click menu acts on it: its URL as written, and the
12/// file that resolves to when it is one.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct PictureMenu {
15    url: String,
16    file: Option<PathBuf>,
17}
18
19/// A row of a picture's right-click menu.
20#[derive(Clone, Copy)]
21enum Action {
22    Open,
23    Reveal,
24    CopyImage,
25    CopyAddress,
26}
27
28impl PictureMenu {
29    /// The picture at `url`, a relative path joined onto `base` as
30    /// [`crate::image_source`] joins it. A URL with a scheme names no file.
31    pub fn new(url: &str, base: Option<&Path>) -> Self {
32        let file = (!url.contains("://")).then(|| match base {
33            Some(base) => base.join(url),
34            None => PathBuf::from(url),
35        });
36        Self {
37            url: url.to_owned(),
38            file,
39        }
40    }
41
42    /// The rows, `None` for a separator — what [`Self::items`] paints and a
43    /// chosen row indexes.
44    fn actions(&self) -> Vec<Option<Action>> {
45        let mut actions = Vec::new();
46        if self.file.is_some() {
47            actions.extend([Some(Action::Open), Some(Action::Reveal), None]);
48            if self.format().is_some() {
49                actions.push(Some(Action::CopyImage));
50            }
51        }
52        actions.push(Some(Action::CopyAddress));
53        actions
54    }
55
56    pub fn items(&self) -> Vec<ui::menu::Item> {
57        self.actions()
58            .into_iter()
59            .map(|action| match action {
60                None => ui::menu::Item::Separator,
61                Some(Action::Open) => ui::menu::Item::action("Open"),
62                Some(Action::Reveal) => ui::menu::Item::action(if cfg!(target_os = "macos") {
63                    "Reveal in Finder"
64                } else {
65                    "Show in Folder"
66                }),
67                Some(Action::CopyImage) => ui::menu::Item::action("Copy Image"),
68                Some(Action::CopyAddress) => ui::menu::Item::action("Copy Image Address"),
69            })
70            .collect()
71    }
72
73    /// The file's picture format, read off its extension.
74    fn format(&self) -> Option<gpui::ImageFormat> {
75        let extension = self.file.as_ref()?.extension()?.to_str()?.to_lowercase();
76        gpui::ImageFormat::from_mime_type(&format!("image/{extension}"))
77    }
78
79    /// Run the row at `row` of [`Self::items`].
80    pub fn run(&self, row: usize, cx: &mut App) {
81        let Some(Some(action)) = self.actions().get(row).copied() else {
82            return;
83        };
84        match (action, &self.file) {
85            (Action::Open, Some(file)) => cx.open_with_system(file),
86            (Action::Reveal, Some(file)) => cx.reveal_path(file),
87            (Action::CopyImage, Some(file)) => {
88                if let (Some(format), Ok(bytes)) = (self.format(), std::fs::read(file)) {
89                    cx.write_to_clipboard(gpui::ClipboardItem::new_image(
90                        &gpui::Image::from_bytes(format, bytes),
91                    ));
92                }
93            }
94            (Action::CopyAddress, _) => {
95                cx.write_to_clipboard(gpui::ClipboardItem::new_string(self.url.clone()));
96            }
97            _ => {}
98        }
99    }
100}