par-term-settings-ui 0.19.0

Settings UI for par-term terminal emulator
Documentation
use crate::SettingsUI;
use crate::search::SearchTag;
use crate::section::collapsing_section_with_state;
use arboard::Clipboard;
use egui::Color32;
use std::collections::HashSet;

use super::cursor_shader_metadata::show_cursor_shader_metadata_and_settings;

pub fn show_cursor_shader(
    ui: &mut egui::Ui,
    settings: &mut SettingsUI,
    changes_this_frame: &mut bool,
    collapsed: &mut HashSet<String>,
) {
    collapsing_section_with_state(
        ui,
        "Cursor Shader",
        "cursor_shader",
        true,
        collapsed,
        |ui, collapsed| {
            ui.label("Apply shader effects to cursor (trails, glow, etc.)");
            ui.add_space(4.0);

            // Cursor shader selection dropdown
            ui.horizontal(|ui| {
                ui.label("Shader:");
                let selected_text = if settings.temp_cursor_shader.is_empty() {
                    "(none)".to_string()
                } else {
                    settings.temp_cursor_shader.clone()
                };

                let mut shader_changed = false;
                egui::ComboBox::from_id_salt("cursor_shader_select")
                    .selected_text(&selected_text)
                    .width(200.0)
                    .show_ui(ui, |ui| {
                        // Option to select none
                        if ui
                            .selectable_label(settings.temp_cursor_shader.is_empty(), "(none)")
                            .clicked()
                        {
                            settings.temp_cursor_shader.clear();
                            settings.config.shader.cursor_shader = None;
                            shader_changed = true;
                        }

                        // List available cursor shaders (only cursor_* shaders)
                        for shader in &settings.cursor_shaders() {
                            let is_selected = settings.temp_cursor_shader == *shader;
                            if ui.selectable_label(is_selected, shader).clicked() {
                                settings.temp_cursor_shader = shader.clone();
                                settings.config.shader.cursor_shader = Some(shader.clone());
                                shader_changed = true;
                            }
                        }
                    })
                    .response
                    .search_tag(&["cursor_shader"]);

                if shader_changed {
                    settings.has_changes = true;
                    *changes_this_frame = true;
                }

                // Refresh button
                if ui
                    .button("↻")
                    .on_hover_text("Refresh shader list")
                    .clicked()
                {
                    settings.refresh_shaders();
                }
            });

            // Browse button for cursor shader
            ui.horizontal(|ui| {
                if ui
                    .button("Browse...")
                    .on_hover_text("Browse for external shader file")
                    .clicked()
                    && let Some(path) = settings.pick_file_path("Select cursor shader file")
                {
                    settings.temp_cursor_shader = path.clone();
                    settings.config.shader.cursor_shader = Some(path);
                    settings.has_changes = true;
                    *changes_this_frame = true;
                }
            });

            // Show cursor shader compilation error if any
            if let Some(error) = &settings.cursor_shader_editor_error {
                let shader_path =
                    par_term_config::Config::shader_path(&settings.temp_cursor_shader);
                let full_error = format!("File: {}\n\n{}", shader_path.display(), error);
                let error_display = error.clone();

                ui.add_space(4.0);
                egui::Frame::default()
                    .fill(Color32::from_rgb(80, 20, 20))
                    .inner_margin(8.0)
                    .outer_margin(0.0)
                    .corner_radius(4.0)
                    .show(ui, |ui| {
                        ui.horizontal(|ui| {
                            ui.colored_label(
                                Color32::from_rgb(255, 100, 100),
                                "⚠ Cursor Shader Error",
                            );
                            ui.with_layout(
                                egui::Layout::right_to_left(egui::Align::Center),
                                |ui| {
                                    if ui.small_button("Copy").clicked()
                                        && let Ok(mut clipboard) = Clipboard::new()
                                    {
                                        let _ = clipboard.set_text(full_error.clone());
                                    }
                                },
                            );
                        });
                        // Show shader path on its own line
                        ui.label(format!("File: {}", shader_path.display()));
                        ui.separator();
                        // Show error details with word wrap
                        ui.add(
                            egui::TextEdit::multiline(&mut error_display.as_str())
                                .font(egui::TextStyle::Monospace)
                                .desired_width(f32::INFINITY)
                                .desired_rows(3)
                                .interactive(false),
                        );
                    });
                ui.add_space(4.0);
            }

            // Show cursor shader metadata and per-shader settings if a shader is selected
            if !settings.temp_cursor_shader.is_empty() {
                show_cursor_shader_metadata_and_settings(
                    ui,
                    settings,
                    changes_this_frame,
                    collapsed,
                );
            }

            crate::dependent::dependent(
                ui,
                !settings.temp_cursor_shader.is_empty(),
                "Shader: a cursor shader selected",
                |ui| {
                    if ui
                        .checkbox(
                            &mut settings.config.shader.cursor_shader_enabled,
                            "Enable cursor shader",
                        )
                        .search_tag(&["cursor_shader_enabled"])
                        .changed()
                    {
                        settings.has_changes = true;
                        *changes_this_frame = true;
                    }

                    if ui
                        .checkbox(
                            &mut settings.config.shader.cursor_shader_animation,
                            "Enable cursor shader animation",
                        )
                        .search_tag(&["cursor_shader_animation"])
                        .changed()
                    {
                        settings.has_changes = true;
                        *changes_this_frame = true;
                    }

                    ui.horizontal(|ui| {
                        ui.label("Animation speed:");
                        if ui
                            .add(
                                egui::Slider::new(
                                    &mut settings.config.shader.cursor_shader_animation_speed,
                                    0.0..=5.0,
                                )
                                .suffix("×"),
                            )
                            .search_tag(&["cursor_shader_animation_speed"])
                            .changed()
                        {
                            settings.has_changes = true;
                            *changes_this_frame = true;
                        }
                        crate::reset::reset_button(ui, settings, changes_this_frame, |c| {
                            &mut c.shader.cursor_shader_animation_speed
                        });
                    });

                    if ui
            .checkbox(
                &mut settings.config.shader.cursor_shader_hides_cursor,
                "Hide default cursor (let shader handle it)",
            ).search_tag(&["cursor_shader_hides_cursor"])
            .on_hover_text("When enabled, the normal cursor is not drawn, allowing the cursor shader to fully replace cursor rendering")
            .changed()
        {
            settings.has_changes = true;
            *changes_this_frame = true;
        }

                    if ui
            .checkbox(
                &mut settings.config.shader.cursor_shader_disable_in_alt_screen,
                "Disable cursor shader in alt screen (vim/less/htop)",
            ).search_tag(&["cursor_shader_disable_in_alt_screen"])
            .on_hover_text("When enabled, cursor shader effects pause while an application is using the alt screen")
            .changed()
        {
            settings.has_changes = true;
            *changes_this_frame = true;
        }

                    ui.horizontal(|ui| {
                        ui.label("Cursor color:");
                        let mut color = settings.config.shader.cursor_shader_color;
                        if ui
                            .color_edit_button_srgb(&mut color)
                            .search_tag(&["cursor_shader_color"])
                            .on_hover_text(
                                "Color passed to cursor shader via iCursorShaderColor uniform",
                            )
                            .changed()
                        {
                            settings.config.shader.cursor_shader_color = color;
                            settings.has_changes = true;
                            *changes_this_frame = true;
                        }
                    });

                    show_trail_and_glow(ui, settings, changes_this_frame);

                    ui.add_space(8.0);
                },
            );
            // Edit Shader button - only enabled when a shader path is set
            let has_shader_path = !settings.temp_cursor_shader.is_empty();
            ui.horizontal(|ui| {
                let edit_button =
                    ui.add_enabled(has_shader_path, egui::Button::new("Edit Cursor Shader..."));
                if edit_button.clicked() {
                    // Load shader source from file
                    let shader_path =
                        par_term_config::Config::shader_path(&settings.temp_cursor_shader);
                    match std::fs::read_to_string(&shader_path) {
                        Ok(source) => {
                            settings.cursor_shader_editor_source = source.clone();
                            settings.cursor_shader_editor_original = source;
                            settings.cursor_shader_editor_error = None;
                            settings.cursor_shader_editor_visible = true;
                        }
                        Err(e) => {
                            settings.cursor_shader_editor_error = Some(format!(
                                "Failed to read cursor shader file '{}': {}",
                                shader_path.display(),
                                e
                            ));
                        }
                    }
                }
                if !has_shader_path {
                    ui.label("(set shader path first)");
                }
            });
        },
    );
}

/// Trail and glow values passed to every cursor shader as uniforms
/// (`iCursorTrailDuration`, `iCursorGlowRadius`, `iCursorGlowIntensity`).
/// A shader's own metadata defaults and per-shader overrides take
/// precedence over these.
fn show_trail_and_glow(
    ui: &mut egui::Ui,
    settings: &mut SettingsUI,
    changes_this_frame: &mut bool,
) {
    ui.horizontal(|ui| {
        ui.label("Trail duration:");
        if ui
            .add(
                egui::Slider::new(
                    &mut settings.config.shader.cursor_shader_trail_duration,
                    0.0..=3.0,
                )
                .suffix(" s"),
            )
            .search_tag(&["cursor_shader_trail_duration"])
            .on_hover_text("How long the cursor trail lasts (iCursorTrailDuration uniform)")
            .changed()
        {
            settings.has_changes = true;
            *changes_this_frame = true;
        }
        crate::reset::reset_button(ui, settings, changes_this_frame, |c| {
            &mut c.shader.cursor_shader_trail_duration
        });
    });

    ui.horizontal(|ui| {
        ui.label("Glow radius:");
        if ui
            .add(
                egui::Slider::new(
                    &mut settings.config.shader.cursor_shader_glow_radius,
                    0.0..=300.0,
                )
                .suffix(" px"),
            )
            .search_tag(&["cursor_shader_glow_radius"])
            .on_hover_text("Radius of the cursor glow (iCursorGlowRadius uniform)")
            .changed()
        {
            settings.has_changes = true;
            *changes_this_frame = true;
        }
        crate::reset::reset_button(ui, settings, changes_this_frame, |c| {
            &mut c.shader.cursor_shader_glow_radius
        });
    });

    ui.horizontal(|ui| {
        ui.label("Glow intensity:");
        if ui
            .add(crate::units::percent(egui::Slider::new(
                &mut settings.config.shader.cursor_shader_glow_intensity,
                0.0..=1.0,
            )))
            .search_tag(&["cursor_shader_glow_intensity"])
            .on_hover_text("Strength of the cursor glow (iCursorGlowIntensity uniform)")
            .changed()
        {
            settings.has_changes = true;
            *changes_this_frame = true;
        }
        crate::reset::reset_button(ui, settings, changes_this_frame, |c| {
            &mut c.shader.cursor_shader_glow_intensity
        });
    });
}