simple_editor\ui/
shader_ui.rs1use crate::model::{Id, DEFAULT_SHADER};
8use eframe::egui;
9
10const UNIFORMS: &str = "sampler2D tex — the layer\n\
12vec2 u_res — layer size in pixels\n\
13float u_time — clip-local seconds\n\
14float u_scale — preview scale (multiply pixel-sized amounts by it)\n\
15float p0..p7 — the eight knobs, also named u1..u8\n\
16sampler2D u_mask, int u_has_mask — the effect's mask (applied for you)";
17
18#[derive(Default)]
19pub struct ShaderUi {
20 pub open: bool,
21 pub target: Option<(Id, usize)>,
23 pub src: String,
24 pub error: String,
26}
27
28impl ShaderUi {
29 pub fn edit(&mut self, clip: Id, index: usize, src: &str) {
31 self.open = true;
32 self.target = Some((clip, index));
33 self.src = src.to_string();
34 self.error.clear();
35 }
36}
37
38pub fn show(ctx: &egui::Context, state: &mut ShaderUi) -> bool {
40 if !state.open {
41 return false;
42 }
43 if state.src.trim().is_empty() {
44 state.src = DEFAULT_SHADER.to_string(); }
46 let mut open = state.open;
47 let mut apply = false;
48 egui::Window::new("Shader Editor").open(&mut open).default_width(560.0).show(ctx, |ui| {
49 ui.weak("Define vec4 effect(vec4 src, vec2 uv) — uv is 0..1, colours are straight alpha.");
50 egui::ScrollArea::vertical().id_salt("shader_src").max_height(280.0).show(ui, |ui| {
51 ui.add(
52 egui::TextEdit::multiline(&mut state.src).code_editor().desired_rows(16).desired_width(f32::INFINITY),
53 );
54 });
55 ui.horizontal(|ui| {
56 apply = ui.button("Apply").on_hover_text("Compile this source and use it").clicked();
57 if ui.button("Reset").on_hover_text("Back to the default body").clicked() {
58 state.src = DEFAULT_SHADER.to_string();
59 }
60 });
61 ui.collapsing("Uniforms", |ui| ui.weak(UNIFORMS));
62 if !state.error.is_empty() {
63 let red = ui.visuals().error_fg_color;
64 ui.colored_label(red, "GLSL error");
65 egui::ScrollArea::vertical().id_salt("shader_err").max_height(160.0).show(ui, |ui| {
66 let mut log = state.error.as_str();
68 ui.add(egui::TextEdit::multiline(&mut log).code_editor().text_color(red).desired_width(f32::INFINITY));
69 });
70 }
71 });
72 state.open = open;
73 apply
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 fn painted(out: &egui::FullOutput) -> String {
82 out.shapes
83 .iter()
84 .filter_map(|c| match &c.shape {
85 egui::epaint::Shape::Text(t) => Some(t.galley.text().to_string()),
86 _ => None,
87 })
88 .collect::<Vec<_>>()
89 .join("\n")
90 }
91
92 #[test]
95 fn show_headless_reports_the_glsl_log() {
96 let ctx = egui::Context::default();
97 let mut st = ShaderUi::default();
98 st.edit(7, 0, "");
99 st.error = "0(12) : error C1503: undefined variable \"foo\"".into();
100 let _ = ctx.run(egui::RawInput::default(), |ctx| assert!(!show(ctx, &mut st)));
101 let out = ctx.run(egui::RawInput::default(), |ctx| assert!(!show(ctx, &mut st)));
103 assert!(st.open && st.target == Some((7, 0)));
104 assert_eq!(st.src, DEFAULT_SHADER, "an empty body is seeded with the working default");
105 let text = painted(&out);
106 assert!(text.contains("undefined variable"), "the log must be shown verbatim: {text}");
107 assert!(text.contains("Apply") && text.contains("vec4 effect"), "{text}");
108 }
109
110 #[test]
112 fn closed_window_draws_nothing() {
113 let ctx = egui::Context::default();
114 let mut st = ShaderUi::default();
115 let out = ctx.run(egui::RawInput::default(), |ctx| assert!(!show(ctx, &mut st)));
116 assert!(!painted(&out).contains("Apply"));
117 }
118}