use concinnity_core::components::ShaderStage;
use concinnity_core::ecs::World;
use std::cell::Cell;
use std::path::Path;
use crate::debug::hot_reload::{ReloadOutcome, ReloadReport, ReloadSubject};
use crate::editor::hook::EditorHook;
use crate::editor::hook::tests::fixtures::hook;
use crate::editor::modal;
use crate::editor::panels::registry::{self, PanelKey};
use crate::editor::panels::shader_list::RowKind;
use crate::editor::panels::shader_list_panel::ShadersAction;
use crate::editor::panels::shader_source::{self, Leave, SourceKey};
fn shader(name: &str, fragment: &Path) -> serde_json::Value {
serde_json::json!({"type": "Shader", "args": {
"$id": name,
"fragment": fragment.to_string_lossy(),
}})
}
fn key(shader: &str) -> SourceKey {
SourceKey::shader(shader, ShaderStage::Fragment)
}
fn session(dir: &Path) -> EditorHook {
let lit = dir.join("lit.hlsl");
let water = dir.join("water.hlsl");
std::fs::write(&lit, "lit").unwrap();
std::fs::write(&water, "water").unwrap();
hook(vec![shader("lit", &lit), shader("water", &water)])
}
fn open_shader(h: &EditorHook) -> Option<String> {
h.shaders.source.as_ref().and_then(|s| match &s.key {
SourceKey::Shader { name, .. } => Some(name.clone()),
SourceKey::Field { .. } => None,
})
}
fn modal_actions(h: &EditorHook) -> Vec<modal::Action> {
h.modal
.as_ref()
.map(|m| m.buttons.iter().map(|b| b.action.clone()).collect())
.unwrap_or_default()
}
#[test]
fn a_file_row_opens_the_source_panel() {
let dir = tempfile::tempdir().unwrap();
let mut h = session(dir.path());
let mut world = World::new();
let rows = h.shader_rows().to_vec();
let i = rows
.iter()
.position(|r| r.kind == RowKind::File(key("water")))
.unwrap();
h.apply_shaders_action(ShadersAction::Row(i), &rows, &mut world);
let src = h.shaders.source.as_ref().unwrap();
assert_eq!(src.area.text(), "water");
assert_eq!(src.title(), "water fragment");
assert!(src.focus);
assert_eq!(h.panel_order.last(), Some(&PanelKey::ShaderSource));
assert!(registry::panel(PanelKey::ShaderSource).is_open(&h));
}
#[test]
fn leaving_unsaved_edits_asks_first() {
let dir = tempfile::tempdir().unwrap();
let mut h = session(dir.path());
h.open_shader_file(key("lit"));
h.shaders.source.as_mut().unwrap().area.type_char('x');
h.open_shader_file(key("water"));
assert_eq!(open_shader(&h).as_deref(), Some("lit"), "not left yet");
let actions = modal_actions(&h);
assert_eq!(actions.len(), 3);
assert_eq!(actions[0], modal::Action::Dismiss);
h.modal = None;
h.answer_leave_shader_source(false, Leave::Open(key("water")), &mut World::new());
assert_eq!(open_shader(&h).as_deref(), Some("water"));
assert_eq!(
std::fs::read_to_string(dir.path().join("lit.hlsl")).unwrap(),
"lit"
);
h.shaders.source.as_mut().unwrap().area.type_char('y');
h.answer_leave_shader_source(true, Leave::Open(key("lit")), &mut World::new());
assert_eq!(
std::fs::read_to_string(dir.path().join("water.hlsl")).unwrap(),
"ywater"
);
assert_eq!(open_shader(&h).as_deref(), Some("lit"));
}
#[test]
fn reopening_the_open_file_keeps_its_edits() {
let dir = tempfile::tempdir().unwrap();
let mut h = session(dir.path());
h.open_shader_file(key("lit"));
h.shaders.source.as_mut().unwrap().area.type_char('x');
h.open_shader_file(key("lit"));
assert!(h.modal.is_none());
assert_eq!(h.shaders.source.as_ref().unwrap().area.text(), "xlit");
}
#[test]
fn closing_asks_only_over_unsaved_edits() {
let dir = tempfile::tempdir().unwrap();
let mut h = session(dir.path());
let mut world = World::new();
let panel = registry::panel(PanelKey::ShaderSource);
h.open_shader_file(key("lit"));
h.shaders.source.as_mut().unwrap().area.type_char('x');
panel.close(&mut h, &mut world);
assert!(panel.is_open(&h));
assert_eq!(
modal_actions(&h)[1],
modal::Action::LeaveShaderSource {
save: false,
then: Leave::Close
}
);
h.modal = None;
h.answer_leave_shader_source(false, Leave::Close, &mut world);
assert!(!panel.is_open(&h));
h.open_shader_file(key("lit"));
panel.close(&mut h, &mut world);
assert!(!panel.is_open(&h));
assert!(h.modal.is_none());
}
#[test]
fn a_save_waits_on_its_recompile() {
let dir = tempfile::tempdir().unwrap();
let mut h = session(dir.path());
h.shaders
.reports
.arm([ReloadSubject::shader("lit"), ReloadSubject::shader("water")]);
h.open_shader_file(key("lit"));
h.drive_shader_source();
h.shaders.source.as_mut().unwrap().area.type_char('x');
assert!(h.save_shader_source());
let src = h.shaders.source.as_ref().unwrap();
assert!(!src.area.is_dirty());
assert!(src.compiling);
assert_eq!(src.status.as_ref().unwrap().text, "Compiling...");
let path = src.path.clone();
h.shaders.reports.publish(&[ReloadReport {
subject: ReloadSubject::shader("lit"),
outcome: failed_at(&path, 1, 3),
}]);
h.drive_shader_source();
let src = h.shaders.source.as_ref().unwrap();
assert!(!src.compiling);
assert_eq!(src.markers.len(), 1);
assert_eq!(src.area.caret().col, 2, "the caret jumped to the error");
}
fn failed_at(path: &str, line: u32, column: u32) -> ReloadOutcome {
use crate::debug::hot_reload::ReloadFailure;
use concinnity_cook::compile::program::{CompileFailure, Diagnostic, Severity};
ReloadOutcome::Failed(ReloadFailure::Compile(CompileFailure {
owner: "Shader 'lit'".to_string(),
failures: Vec::new(),
diagnostics: vec![Diagnostic {
path: path.to_string(),
line,
column,
severity: Severity::Error,
message: "bad".to_string(),
context: String::new(),
}],
hint: "",
}))
}
thread_local! {
static RESOLVED: Cell<usize> = const { Cell::new(0) };
}
fn counting_resolve(declared: &str, dir: Option<&Path>) -> String {
RESOLVED.with(|n| n.set(n.get() + 1));
shader_source::resolve_path(declared, dir)
}
#[test]
fn the_list_resolves_each_file_once() {
let _guard = crate::test_support::lock();
let dir = tempfile::tempdir().unwrap();
let mut h = session(dir.path());
h.shaders.resolve = counting_resolve;
RESOLVED.with(|n| n.set(0));
h.shaders
.reports
.arm([ReloadSubject::shader("lit"), ReloadSubject::shader("water")]);
for _ in 0..5 {
h.shader_rows();
}
assert_eq!(RESOLVED.with(Cell::get), 2, "one per declared file");
let lit = dir.path().join("lit.hlsl").to_string_lossy().into_owned();
h.shaders.reports.publish(&[ReloadReport {
subject: ReloadSubject::shader("lit"),
outcome: failed_at(&lit, 1, 1),
}]);
let badge = h
.shader_rows()
.iter()
.find(|r| r.kind == RowKind::File(key("lit")))
.and_then(|r| r.badge.clone())
.unwrap();
assert_eq!(badge.0, "failed");
assert_eq!(RESOLVED.with(Cell::get), 2);
}