hgame 0.26.4

CG production management structs, e.g. of assets, personnels, progress, etc.
Documentation
use super::*;

#[cfg(feature = "gui")]
use egui::Button;

#[cfg(feature = "gui")]
use mkutil::finder;

#[cfg(feature = "image_processing")]
/// The ratio below which we'll still scale the image using a given `thumbnail width`.
const MAX_TALL_IMAGE_RATIO: f32 = 1.15;

// ----------------------------------------------------------------------------
#[derive(Debug)]
/// A button with image, and when clicked it opens the stored path.
pub struct ImageLink {
    url: Option<PathBuf>,

    #[cfg(feature = "gui")]
    button: egui::ImageButton,
}

#[cfg(feature = "gui")]
impl egui::Widget for ImageLink {
    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
        let ImageLink { url, button } = self;
        let response = button.ui(ui);
        if let Some(url) = url {
            if response.clicked() {
                // reveal is suffice
                if let Err(e) = finder::reveal(&url) {
                    error!("{}", e);
                };
            };

            #[cfg(debug_assertions)]
            return response.on_hover_text(format!("{}", url.display()));

            #[cfg(not(debug_assertions))]
            return response;
        } else {
            response
        }
    }
}

#[cfg(all(feature = "image_processing", feature = "gui"))]
impl ImageLink {
    pub fn with_retained_image(
        ctx: &egui::Context,
        img: &RetainedImage,
        url: Option<&Path>,
        thumb_width: f32,
    ) -> Self {
        Self {
            url: url.map(|p| p.to_path_buf()),
            button: ImageButton::new(
                img.texture_id(ctx),
                [
                    thumb_width,
                    (img.size_vec2().y / img.size_vec2().x) * thumb_width,
                ],
            ),
        }
    }

    /// If the image height-width ratio is above a certain threshold, then
    /// we show the image actual size.
    pub fn with_retained_image_actual_height(
        ctx: &egui::Context,
        img: &RetainedImage,
        url: Option<&Path>,
        thumb_width: f32,
    ) -> Self {
        let ratio = img.size_vec2().y / img.size_vec2().x;

        Self {
            url: url.map(|p| p.to_path_buf()),
            button: if let Ordering::Less = ratio.total_cmp(&MAX_TALL_IMAGE_RATIO) {
                // wide or almost square images
                ImageButton::new(img.texture_id(ctx), [thumb_width, ratio * thumb_width])
            } else {
                // tall images
                ImageButton::new(img.texture_id(ctx), img.size_vec2())
            },
        }
    }
}

// ----------------------------------------------------------------------------
/// A button that shows File Explorer (on Windows)
/// and reveals Finder (on MacOS).
pub struct FinderLink {
    url: PathBuf,

    #[cfg(feature = "gui")]
    button: Button,
}

#[cfg(feature = "gui")]
impl egui::Widget for FinderLink {
    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
        let FinderLink { url, button } = self;
        let response = button.ui(ui);
        if response.clicked() {
            // reveal is suffice
            if let Err(e) = finder::reveal(&url) {
                error!("{}", e);
            };
        };
        response.on_hover_text(format!("{}", url.display()))
    }
}

impl FinderLink {
    /// If no label is given then uses the file name of the given path.
    pub fn with_url(_label: Option<&str>, url: &Path) -> Self {
        Self {
            url: url.to_path_buf(),

            #[cfg(feature = "gui")]
            button: Button::new(match _label {
                Some(label) => label,
                None => url
                    .file_name()
                    .unwrap_or(std::ffi::OsStr::new("😷 broken file name"))
                    .to_str()
                    .unwrap(),
            }),
        }
    }
}

// ----------------------------------------------------------------------------
/// A button that opens a player to view image sequence.
pub struct TurntableLink<T>
where
    T: Fn(),
{
    #[cfg(feature = "gui")]
    button: Button,

    launch_player: T,
}

#[cfg(feature = "gui")]
impl<T> egui::Widget for TurntableLink<T>
where
    T: Fn(),
{
    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
        let TurntableLink {
            button,
            launch_player,
        } = self;
        let response = button.ui(ui);
        if response.clicked() {
            launch_player();
        };
        response.on_hover_text("Open image sequence player")
    }
}
impl<T> TurntableLink<T>
where
    T: Fn(),
{
    /// `player`, for example, calling DJV, is what implements Fn trait.
    pub fn with_player(_label: &str, launch_player: T) -> Self {
        Self {
            launch_player,
            #[cfg(feature = "gui")]
            button: Button::new(_label),
        }
    }
}

// ----------------------------------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalLink {
    /// UI metadata used to switch the read/write UI of a link.
    #[serde(skip)]
    mode: MediaMode,

    name: String,

    #[cfg(feature = "gui")]
    icon_key: String,

    url: String,
}

impl ReadWriteSuggest for ExternalLink {
    fn write_suggest() -> Self {
        Self::empty().with_mode(MediaMode::WriteSuggest)
    }

    fn with_mode(mut self, mode: MediaMode) -> Self {
        self.mode_mut(mode);
        self
    }

    fn mode(&self) -> &MediaMode {
        &self.mode
    }

    fn mode_mut(&mut self, mode: MediaMode) {
        self.mode = mode;
    }

    #[cfg(feature = "gui")]
    fn write_compose_ui(&mut self, ui: &mut egui::Ui) {
        ui.group(|ui| {
            ui.vertical(|ui| {
                // `egui::Grid` won't show the `egui::TextEdit` with `desired_width`.
                // Name
                ui.horizontal(|ui| {
                    ui.monospace("Name:");
                    ui.add(
                        egui::TextEdit::singleline(&mut self.name)
                            .hint_text("Nice name of the new external link")
                            .desired_width(300.),
                    );
                });
                // URL
                ui.horizontal(|ui| {
                    ui.monospace("URL: ");
                    ui.add(egui::TextEdit::singleline(&mut self.url).desired_width(300.));
                });
                // shows all available icons via HashMap key
                #[cfg(feature = "image_processing")]
                IconCel::icon_book().select_public_icon_key_ui(&mut self.icon_key, ui);
            });
        });
    }
}

impl ExternalLink {
    /// Using `embedded_icons::DEFAULT_LINK` for `Self::icon_key`.
    pub fn empty() -> Self {
        Self {
            mode: MediaMode::default(),
            name: String::new(),
            #[cfg(feature = "gui")]
            icon_key: embedded_icons::DEFAULT_LINK.to_owned(),
            url: String::new(),
        }
    }

    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_owned(),
            ..Self::empty()
        }
    }

    pub fn name(&self) -> &String {
        &self.name
    }

    pub fn with_url(mut self, url: &str) -> Self {
        self.url = url.to_owned();
        self
    }

    #[cfg(feature = "gui")]
    pub fn with_icon_key(mut self, key: &str) -> Self {
        self.icon_key = key.to_owned();
        self
    }
}

#[cfg(feature = "gui")]
impl ExternalLink {
    pub fn read_mode_ui_with_size(&mut self, ui: &mut egui::Ui, _size: impl Into<egui::Vec2>) {
        #[cfg(feature = "image_processing")]
        {
            let button = egui::ImageButton::new(
                IconCel::icon_book()
                    .get_or_default(&self.icon_key)
                    .texture_id(ui.ctx()),
                _size,
            );
            if button
                .ui(ui)
                .on_hover_text(format!("{}: {}", self.name, self.url))
                .clicked()
            {
                ui.ctx()
                    .output_mut(|o| o.open_url = Some(egui::output::OpenUrl::new_tab(&self.url)));
            };
        }

        #[cfg(not(feature = "image_processing"))]
        if ui.button(&self.name).on_hover_text(&self.url).clicked() {
            ui.ctx().output_mut(|o| {
                o.open_url = Some(egui::output::OpenUrl::new_tab(&self.url));
            });
        };
    }

    fn write_suggest_ui_with_size(&mut self, ui: &mut egui::Ui, _size: impl Into<egui::Vec2>) {
        #[cfg(feature = "image_processing")]
        {
            let button = egui::ImageButton::new(
                IconCel::icon_book()
                    .get_included(embedded_icons::ADD_LINK)
                    .texture_id(ui.ctx()),
                _size,
            );
            if button
                .ui(ui)
                .on_hover_text("Add a new external link")
                .clicked()
            {
                self.mode = MediaMode::WriteCompose;
            };
        }
        #[cfg(not(feature = "image_processing"))]
        {
            if ui
                .button("➕ New Link")
                .on_hover_text("Add a new external link")
                .clicked()
            {
                self.mode = MediaMode::WriteCompose;
            };
        }
    }

    /// Depending on `Self::mode` to show the UI for reading, or the UI for editing.
    pub fn ui(&mut self, ui: &mut egui::Ui, size: impl Into<egui::Vec2>) {
        match &self.mode {
            MediaMode::Read => self.read_mode_ui_with_size(ui, size),
            MediaMode::WriteSuggest => {
                self.write_suggest_ui_with_size(ui, size);
            }
            MediaMode::WriteCompose => {
                self.write_compose_ui(ui);
            }
            MediaMode::WriteEdit => {
                self.write_edit_ui(ui);
            }
        }
    }
}

// -------------------------------------------------------------------------------
#[async_trait]
/// Interface to how external URLs of a project are found.
pub trait MakeWebLink: DynClone + fmt::Debug + Send + Sync {
    /// Gets all the links.
    async fn external_links(&self, project: &Project) -> Result<Vec<ExternalLink>, DatabaseError>;
    /// Adds a new link.
    async fn add_link(
        &self,
        project: &Project,
        link: &ExternalLink,
    ) -> Result<(), ModificationError>;
    /// Deletes an existing link.
    async fn delete_link(
        &self,
        project: &Project,
        link: &ExternalLink,
    ) -> Result<(), ModificationError>;
    /// Updates an existing link at given index.
    async fn edit_link(
        &self,
        project: &Project,
        index: usize,
        new: &ExternalLink,
    ) -> Result<(), ModificationError>;
}

dyn_clone::clone_trait_object!(MakeWebLink);

#[cfg(test)]
mod tests {
    // use super::*;
    // #[test]
    // fn replace_root_dir() {
    // 	let path = PathBuf::from("//vnnas/projects/WOOKIEES/data/feedback/APO/2021-09-06/DeathTrooper/deathtrooperv3_feedback.jpg");
    // 	let _ret = FinderLink::with_mocking_url(0, &path);
    // }
}