simple_editor\ui/
shader_ui.rs

1//! "Shader Editor" window for `EffectKind::Shader`: the GLSL body in a monospace editor, an Apply
2//! button, and the driver's compile log verbatim (selectable, so it can be pasted somewhere) when the
3//! source was rejected. Non-modal — the timeline stays usable while it is open.
4//! The window only edits text: the app writes the source back onto the effect, pushes undo and asks
5//! `GpuRenderer::check_shader` for the log, then hands it here through `error`.
6
7use crate::model::{Id, DEFAULT_SHADER};
8use eframe::egui;
9
10/// What `shaders::user_shader` puts in scope — the knobs are invisible otherwise.
11const 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    /// Clip + index in its effect stack being edited.
22    pub target: Option<(Id, usize)>,
23    pub src: String,
24    /// The driver's log for the last applied source; empty once it links.
25    pub error: String,
26}
27
28impl ShaderUi {
29    /// Point the window at one effect's source (the Effects panel's "Edit shader…").
30    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
38/// Draws the window; true = "Apply" (the app writes the source back and compiles it).
39pub 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(); // an empty buffer teaches nobody the entry point
45    }
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                // read-only TextEdit: the log stays selectable and verbatim, wrapping and all
67                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    /// Text painted in one frame (the same trick library.rs uses to find a button).
81    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    /// Headless: the window lays out, applies nothing on its own, seeds an empty body, and prints the
93    /// driver's log verbatim when the app hands one back.
94    #[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        // second frame: the window knows its size and the collapsing header is settled
102        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    /// Closing the window (or never opening it) is a no-op.
111    #[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}