hgame 0.26.4

CG production management structs, e.g. of assets, personnels, progress, etc.
Documentation
mod deli;
mod graph;
mod template;
mod workflow;

pub use graph::*;
pub use template::*;
pub use workflow::*;

use super::*;
#[cfg(all(feature = "easy_mark", feature = "gui"))]
use egui_demo_lib::easy_mark;
use mkutil::{
    dag::{self, NamedNode},
    text,
};

#[async_trait]
pub trait WorkflowDefinition: DeliveryStages + ProjectDefaultDelivery {}

dyn_clone::clone_trait_object!(WorkflowDefinition);

// -------------------------------------------------------------------------------
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
/// Delivery stage, of which a collection can form a DAG.
pub struct Stage {
    #[serde(skip)]
    mode: MediaMode,

    #[serde(skip)]
    /// This should match to the original index of the `Stage` in the document in DB,
    /// i.e. with regard to deser fails.
    pub index: Option<StageIndex>,

    #[serde(skip)]
    /// Whether the active `ProductionAsset` is in this `Stage`.
    pub has_reference_to: bool,

    pub name: String,

    pub description: String,

    // #[serde(default)]
    // details: String,
    is_internal: bool,

    #[cfg(feature = "gui")]
    pub color: Color32,

    /// Elements are names of other `Stage`s, so that we can form a DAG after deser.
    depends_on: Vec<String>,

    #[serde(skip)]
    /// Used for text edit when drafting.
    depends_on_draft: Option<String>,
}

impl Stage {
    fn empty() -> Self {
        Self {
            #[cfg(feature = "gui")]
            color: Color32::GRAY,
            ..Default::default()
        }
    }

    fn draft() -> Self {
        let mut stage = Self {
            mode: MediaMode::WriteCompose,
            ..Self::empty()
        };
        stage.init_empty_draft();
        stage
    }

    fn init_empty_draft(&mut self) {
        if self.depends_on_draft.is_none() {
            self.depends_on_draft = Some(String::new());
        };
    }

    fn init_draft_from_existing(&mut self) {
        if self.depends_on_draft.is_none() {
            self.depends_on_draft = Some(self.depends_on.join(", "));
        };
    }

    fn parse_dependency_draft(&mut self) {
        if let Some(draft) = &self.depends_on_draft {
            self.depends_on = text::relaxed_from_comma_sep_singleline(draft, 50);
        };
    }

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

    pub fn depends_on(mut self, parent: &str) -> Self {
        self.depends_on.push(parent.to_owned());
        self
    }
}

#[cfg(feature = "gui")]
/// All UI-related methods.
impl Stage {
    fn name_and_hint(&self, ui: &mut egui::Ui) {
        if self.is_internal {
            ui.heading(format!("{} 🏠", self.name))
                .on_hover_text(RichText::new("Internal").color(Color32::LIGHT_GREEN))
        } else {
            ui.heading(format!("{} 🌏", self.name))
                .on_hover_text(RichText::new("Client-facing").color(Color32::LIGHT_RED))
        };
    }

    fn depends_on_ui(&self, ui: &mut egui::Ui) {
        // `depends_on`, should not use `Self::depends_on_draft`
        if self.depends_on.is_empty() {
            ui.weak("Depends on nothing");
        } else {
            ui.strong(format!("Depends on: {}", self.depends_on.join(", ")));
        };
    }

    pub fn color_ui(&self, ui: &mut egui::Ui) {
        ui.colored_label(self.color, "");
    }

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

impl NamedNode for Stage {
    fn name(&self) -> &String {
        &self.name
    }
}

impl ReadWriteSuggest for Stage {
    fn write_suggest() -> Self {
        Self {
            mode: MediaMode::WriteSuggest,
            depends_on_draft: Some(String::new()),
            ..Self::empty()
        }
    }

    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 read_mode_ui(&mut self, ui: &mut egui::Ui) {
        ui.group(|ui| {
            ui.horizontal(|ui| {
                // color
                #[cfg(feature = "persistence")]
                self.color_ui(ui);
                // name
                self.name_and_hint(ui);
            });

            // description
            #[cfg(feature = "easy_mark")]
            // supports for Easy Mark syntax
            easy_mark::easy_mark(ui, &self.description);

            #[cfg(not(feature = "easy_mark"))]
            ui.label(&self.description);

            self.depends_on_ui(ui);
        });
    }

    #[cfg(feature = "gui")]
    /// SAFETY: caller is responsible for making sure `Self::depends_on_draft` `is_some`.
    fn write_compose_ui(&mut self, ui: &mut egui::Ui) {
        // color
        ui.horizontal(|ui| {
            ui.label("Tag:");
            ui.color_edit_button_srgba(&mut self.color);
        });

        // public/private
        let text = if self.is_internal {
            RichText::new("Internal").color(Color32::LIGHT_GREEN)
        } else {
            RichText::new("Client-facing").color(Color32::LIGHT_RED)
        };
        ui.checkbox(&mut self.is_internal, text)
            .on_hover_text("Stages marked as NOT Internal are used for Outgoing Delivery");

        // name
        ui.horizontal(|ui| {
            ui.label("Stage name:");
            ui.add(
                egui::TextEdit::singleline(&mut self.name)
                    .hint_text("Special characters are allowed"),
            );
        });

        // description
        ui.horizontal(|ui| {
            #[cfg(feature = "easy_mark")]
            {
                ui.label("Description:")
                    .on_hover_text(crate::locale::EASYMARK_SYNTAX_HELP);

                ui.add(
                    egui::TextEdit::multiline(&mut self.description)
                        .desired_rows(2)
                        .hint_text("EasyMark syntax is supported"),
                );
            }

            #[cfg(not(feature = "easy_mark"))]
            {
                ui.label("Description:");
                ui.add(egui::TextEdit::multiline(&mut self.description).desired_rows(2));
            }
        });

        // depends_on
        ui.horizontal(|ui| {
            ui.label("Depends on:")
                .on_hover_text("Make sure the dependencies' names match");
            ui.add(
                egui::TextEdit::singleline(self.depends_on_draft.as_mut().unwrap())
                    .hint_text("Separated with commas"),
            );
        });
    }
}

// -------------------------------------------------------------------------------
#[derive(Debug, Clone, failure::Fail)]
pub enum GraphError {
    #[fail(display = "Validity of dependency graph is unchecked")]
    NotChecked,

    #[fail(display = "Stage is None")]
    StageIsNone,

    #[fail(display = "Missing root node in dependency graph")]
    NonExistentRoot,

    #[fail(display = "Stage \"{}\" is defined multiple times", _0)]
    /// `_0`: `Stage::name`
    StageNameDuplication(String),

    #[fail(
        display = "Dependency \"{}\" of stage \"{}\" must be defined first",
        _0, _1
    )]
    /// `_0`: Parent node
    /// `_1`: Child node
    UndefinedDependency(String, String),

    #[fail(display = "Failed to add edge between {} and {}: {}", _0, _1, _2)]
    /// `_0`: Parent node
    /// `_1`: Child node
    /// `_2`: `WouldCycle` error
    CycleEdge(String, String, String),
}

impl Into<anyhow::Error> for GraphError {
    fn into(self) -> anyhow::Error {
        anyhow!("{}", self)
    }
}

impl Into<anyhow::Error> for &GraphError {
    fn into(self) -> anyhow::Error {
        anyhow!("{}", self)
    }
}