use concinnity_core::components::ShaderStage;
use std::path::{Path, PathBuf};
use super::shader_edit::ShaderEdit;
use crate::editor::modal;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SourceKey {
Shader { name: String, stage: ShaderStage },
Field { path: String },
}
impl SourceKey {
pub(crate) fn shader(name: &str, stage: ShaderStage) -> Self {
SourceKey::Shader {
name: name.to_string(),
stage,
}
}
}
pub(crate) fn stage_name(stage: ShaderStage) -> &'static str {
match stage {
ShaderStage::Vertex => "vertex",
ShaderStage::Fragment => "fragment",
}
}
pub(crate) fn resolve_path(declared: &str, assets_dir: Option<&Path>) -> String {
concinnity_host::store::source::resolve_source_path(declared, assets_dir)
}
pub(crate) fn resolve_field_path(declared: &str, assets_dir: Option<&Path>) -> String {
concinnity_host::store::source::find_existing(declared, assets_dir).unwrap_or_else(|| {
match assets_dir {
Some(dir) if !Path::new(declared).is_absolute() => {
dir.join(declared).to_string_lossy().into_owned()
}
_ => declared.to_string(),
}
})
}
pub(crate) fn same_file(a: &str, b: &str) -> bool {
if a == b {
return true;
}
match (Path::new(a).canonicalize(), Path::new(b).canonicalize()) {
(Ok(a), Ok(b)) => a == b,
_ => false,
}
}
pub(crate) fn starter_path(dir: &Path, name: &str, exists: impl Fn(&Path) -> bool) -> PathBuf {
let shaders = dir.join("shaders");
let first = shaders.join(format!("{name}.hlsl"));
if !exists(&first) {
return first;
}
(1..)
.map(|i| shaders.join(format!("{name}_{i}.hlsl")))
.find(|p| !exists(p))
.expect("an unbounded search finds a free name")
}
pub(crate) fn declared_form(path: &Path, root: &Path) -> String {
match path.strip_prefix(root) {
Ok(relative) => relative
.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/"),
Err(_) => path.to_string_lossy().into_owned(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Leave {
Open(SourceKey),
Close,
Edit(ShaderEdit),
ConfirmForm,
}
pub(crate) fn must_ask(dirty: bool, open: &SourceKey, then: &Leave) -> bool {
dirty
&& match then {
Leave::Open(key) => key != open,
Leave::Close | Leave::Edit(_) | Leave::ConfirmForm => true,
}
}
pub(crate) fn leave_prompt(file: &str) -> String {
format!("{file} has unsaved changes.")
}
pub(crate) fn leave_buttons(then: Leave) -> Vec<modal::Button> {
vec![
modal::Button {
label: "Cancel".to_string(),
danger: false,
action: modal::Action::Dismiss,
},
modal::Button {
label: "Discard".to_string(),
danger: true,
action: modal::Action::LeaveShaderSource {
save: false,
then: then.clone(),
},
},
modal::Button {
label: "Save".to_string(),
danger: false,
action: modal::Action::LeaveShaderSource { save: true, then },
},
]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DiskChange {
Unchanged,
Reload,
Notice,
}
pub(crate) fn disk_change(
known: &str,
disk: &str,
dirty: bool,
noticed: Option<&str>,
) -> DiskChange {
if disk == known {
DiskChange::Unchanged
} else if !dirty {
DiskChange::Reload
} else if noticed == Some(disk) {
DiskChange::Unchanged
} else {
DiskChange::Notice
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(shader: &str, stage: ShaderStage) -> SourceKey {
SourceKey::shader(shader, stage)
}
#[test]
fn leaving_asks_only_over_unsaved_edits() {
let open = key("water", ShaderStage::Fragment);
let other = Leave::Open(key("water", ShaderStage::Vertex));
assert!(must_ask(true, &open, &other));
assert!(must_ask(true, &open, &Leave::Close));
assert!(!must_ask(false, &open, &other));
assert!(!must_ask(false, &open, &Leave::Close));
assert!(!must_ask(true, &open, &Leave::Open(open.clone())));
let delete = Leave::Edit(ShaderEdit::Delete {
name: "water".to_string(),
files: false,
});
assert!(must_ask(true, &open, &delete));
assert!(!must_ask(false, &open, &delete));
}
#[test]
fn the_leave_buttons_cancel_discard_or_save() {
let then = Leave::Open(key("lit", ShaderStage::Fragment));
let buttons = leave_buttons(then.clone());
let labels: Vec<&str> = buttons.iter().map(|b| b.label.as_str()).collect();
assert_eq!(labels, ["Cancel", "Discard", "Save"]);
assert_eq!(buttons[0].action, modal::Action::Dismiss);
assert_eq!(
buttons[1].action,
modal::Action::LeaveShaderSource {
save: false,
then: then.clone()
}
);
assert!(buttons[1].danger);
assert_eq!(
buttons[2].action,
modal::Action::LeaveShaderSource { save: true, then }
);
}
#[test]
fn a_change_on_disk_reloads_a_clean_buffer_and_notices_a_dirty_one() {
assert_eq!(disk_change("a", "a", true, None), DiskChange::Unchanged);
assert_eq!(disk_change("a", "b", false, None), DiskChange::Reload);
assert_eq!(disk_change("a", "b", true, None), DiskChange::Notice);
assert_eq!(
disk_change("a", "b", true, Some("b")),
DiskChange::Unchanged
);
assert_eq!(disk_change("a", "c", true, Some("b")), DiskChange::Notice);
assert_eq!(disk_change("a", "b", false, Some("b")), DiskChange::Reload);
}
#[test]
fn the_starter_path_numbers_past_existing_files() {
let dir = Path::new("/cn-none/assets");
let taken = [
dir.join("shaders/editor_shader.hlsl"),
dir.join("shaders/editor_shader_1.hlsl"),
];
let exists = |p: &Path| taken.iter().any(|t| t == p);
assert_eq!(
starter_path(dir, "editor_shader", exists),
dir.join("shaders/editor_shader_2.hlsl")
);
assert_eq!(
starter_path(dir, "fresh", exists),
dir.join("shaders/fresh.hlsl")
);
}
#[test]
fn a_declared_path_is_relative_to_the_root_when_under_it() {
let root = Path::new("/cn-none/project");
assert_eq!(
declared_form(&root.join("assets").join("shaders").join("a.hlsl"), root),
"assets/shaders/a.hlsl"
);
assert_eq!(
declared_form(Path::new("/cn-none/elsewhere/a.hlsl"), root),
"/cn-none/elsewhere/a.hlsl"
);
}
#[test]
fn a_field_resolves_under_the_assets_directory() {
let dir = tempfile::tempdir().unwrap();
let shaders = dir.path().join("shaders");
std::fs::create_dir_all(&shaders).unwrap();
std::fs::write(shaders.join("blob.hlsl"), "").unwrap();
let found = resolve_field_path("shaders/blob.hlsl", Some(dir.path()));
assert!(same_file(
&found,
&shaders.join("blob.hlsl").to_string_lossy()
));
let fresh = resolve_field_path("shaders/new.hlsl", Some(dir.path()));
assert_eq!(fresh, dir.path().join("shaders/new.hlsl").to_string_lossy());
let elsewhere = tempfile::tempdir().unwrap();
let absolute = elsewhere
.path()
.join("a.hlsl")
.to_string_lossy()
.into_owned();
assert_eq!(resolve_field_path(&absolute, Some(dir.path())), absolute);
}
#[test]
fn paths_resolve_as_the_renderer_resolves_them() {
let dir = tempfile::tempdir().unwrap();
let nested = dir.path().join("shaders");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(nested.join("lit.hlsl"), "").unwrap();
assert_eq!(
resolve_path("lit.hlsl", Some(dir.path())),
nested.join("lit.hlsl").to_string_lossy()
);
assert_eq!(resolve_path("a/b.hlsl", Some(dir.path())), "a/b.hlsl");
assert!(same_file(
&resolve_path("lit.hlsl", Some(dir.path())),
&nested.join(".").join("lit.hlsl").to_string_lossy()
));
assert!(!same_file("/cn-none/a.hlsl", "/cn-none/b.hlsl"));
}
}