use crate::SettingsUI;
use crate::section::collapsing_section_with_state;
use arboard::Clipboard;
use egui::Color32;
use par_term_config::{BackgroundImageMode, BackgroundMode};
use std::collections::HashSet;
mod cursor_shader;
mod cursor_shader_metadata;
mod global_channels;
mod pane_backgrounds;
mod shader_channel_settings;
mod shader_metadata;
mod shader_settings;
pub use cursor_shader::show_cursor_shader;
pub use pane_backgrounds::show_pane_backgrounds;
pub use shader_settings::show_shader_metadata_and_settings;
use shader_channel_settings::{
show_background_channel0_controls, show_cubemap_controls, show_global_channel_textures,
};
pub fn show_background(
ui: &mut egui::Ui,
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
collapsed: &mut HashSet<String>,
) {
collapsing_section_with_state(
ui,
"Background & Effects",
"background_effects",
true,
collapsed,
|ui, collapsed| {
ui.horizontal(|ui| {
ui.label("Background mode:");
let current = match settings.config.background_mode {
BackgroundMode::Default => 0,
BackgroundMode::Color => 1,
BackgroundMode::Image => 2,
};
let mut selected = current;
egui::ComboBox::from_id_salt("bg_source_mode")
.selected_text(match current {
0 => "Default (Theme)",
1 => "Solid Color",
2 => "Image",
_ => "Unknown",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut selected, 0, "Default (Theme)");
ui.selectable_value(&mut selected, 1, "Solid Color");
ui.selectable_value(&mut selected, 2, "Image");
});
if selected != current {
settings.config.background_mode = match selected {
0 => BackgroundMode::Default,
1 => BackgroundMode::Color,
2 => BackgroundMode::Image,
_ => BackgroundMode::Default,
};
settings.has_changes = true;
*changes_this_frame = true;
}
});
ui.add_space(4.0);
match settings.config.background_mode {
BackgroundMode::Default => {
ui.label("Using theme background color.");
}
BackgroundMode::Color => {
ui.horizontal(|ui| {
ui.label("Background color:");
let mut color = Color32::from_rgb(
settings.temp_background_color[0],
settings.temp_background_color[1],
settings.temp_background_color[2],
);
if ui.color_edit_button_srgba(&mut color).changed() {
settings.temp_background_color = [color.r(), color.g(), color.b()];
settings.config.background_color = settings.temp_background_color;
settings.has_changes = true;
*changes_this_frame = true;
}
ui.label(format!(
"#{:02X}{:02X}{:02X}",
settings.temp_background_color[0],
settings.temp_background_color[1],
settings.temp_background_color[2]
));
});
ui.label("Transparency controlled by Window Opacity setting.");
}
BackgroundMode::Image => {
ui.horizontal(|ui| {
ui.label("Background image path:");
if ui
.text_edit_singleline(&mut settings.temp_background_image)
.changed()
{
settings.config.background_image =
if settings.temp_background_image.is_empty() {
None
} else {
Some(settings.temp_background_image.clone())
};
settings.has_changes = true;
}
if ui.button("Browse…").clicked()
&& let Some(path) = settings.pick_file_path("Select background image")
{
settings.temp_background_image = path.clone();
settings.config.background_image = Some(path);
settings.has_changes = true;
}
});
if ui
.checkbox(
&mut settings.config.background_image_enabled,
"Enable background image",
)
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
ui.horizontal(|ui| {
ui.label("Background image mode:");
let current = match settings.config.background_image_mode {
BackgroundImageMode::Fit => 0,
BackgroundImageMode::Fill => 1,
BackgroundImageMode::Stretch => 2,
BackgroundImageMode::Tile => 3,
BackgroundImageMode::Center => 4,
};
let mut selected = current;
egui::ComboBox::from_id_salt("bg_mode")
.selected_text(match current {
0 => "Fit",
1 => "Fill",
2 => "Stretch",
3 => "Tile",
4 => "Center",
_ => "Unknown",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut selected, 0, "Fit");
ui.selectable_value(&mut selected, 1, "Fill");
ui.selectable_value(&mut selected, 2, "Stretch");
ui.selectable_value(&mut selected, 3, "Tile");
ui.selectable_value(&mut selected, 4, "Center");
});
if selected != current {
settings.config.background_image_mode = match selected {
0 => BackgroundImageMode::Fit,
1 => BackgroundImageMode::Fill,
2 => BackgroundImageMode::Stretch,
3 => BackgroundImageMode::Tile,
4 => BackgroundImageMode::Center,
_ => BackgroundImageMode::Stretch,
};
settings.has_changes = true;
}
});
ui.horizontal(|ui| {
ui.label("Background image opacity:");
if ui
.add(egui::Slider::new(
&mut settings.config.background_image_opacity,
0.0..=1.0,
))
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
});
}
}
ui.add_space(8.0);
ui.separator();
ui.add_space(4.0);
show_background_shader_controls(ui, settings, changes_this_frame, collapsed);
},
);
}
fn show_background_shader_controls(
ui: &mut egui::Ui,
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
collapsed: &mut HashSet<String>,
) {
ui.horizontal(|ui| {
ui.label("Shader:");
let selected_text = if settings.temp_custom_shader.is_empty() {
"(none)".to_string()
} else {
settings.temp_custom_shader.clone()
};
let mut shader_changed = false;
egui::ComboBox::from_id_salt("shader_select")
.selected_text(&selected_text)
.width(200.0)
.show_ui(ui, |ui| {
if ui
.selectable_label(settings.temp_custom_shader.is_empty(), "(none)")
.clicked()
{
settings.temp_custom_shader.clear();
settings.config.shader.custom_shader = None;
shader_changed = true;
}
for shader in &settings.background_shaders() {
let is_selected = settings.temp_custom_shader == *shader;
if ui.selectable_label(is_selected, shader).clicked() {
settings.temp_custom_shader = shader.clone();
settings.config.shader.custom_shader = Some(shader.clone());
shader_changed = true;
}
}
});
if shader_changed {
settings.has_changes = true;
*changes_this_frame = true;
}
if ui
.button("↻")
.on_hover_text("Refresh shader list")
.clicked()
{
settings.refresh_shaders();
}
});
ui.horizontal(|ui| {
if ui.button("Create New...").clicked() {
settings.new_shader_name.clear();
settings.show_create_shader_dialog = true;
}
let has_shader = !settings.temp_custom_shader.is_empty();
if ui
.add_enabled(has_shader, egui::Button::new("Delete"))
.clicked()
{
settings.show_delete_shader_dialog = true;
}
if ui
.button("Browse...")
.on_hover_text("Browse for external shader file")
.clicked()
&& let Some(path) = settings.pick_file_path("Select shader file")
{
settings.temp_custom_shader = path.clone();
settings.config.shader.custom_shader = Some(path);
settings.has_changes = true;
*changes_this_frame = true;
}
});
if let Some(error) = &settings.shader_editor_error {
let shader_path = par_term_config::Config::shader_path(&settings.temp_custom_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), "âš 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());
}
});
});
ui.label(format!("File: {}", shader_path.display()));
ui.separator();
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);
}
if !settings.temp_custom_shader.is_empty() {
show_shader_metadata_and_settings(ui, settings, changes_this_frame, collapsed);
}
if ui
.checkbox(
&mut settings.config.shader.custom_shader_enabled,
"Enable custom shader",
)
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
if ui
.checkbox(
&mut settings.config.shader.custom_shader_animation,
"Enable 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.custom_shader_animation_speed,
0.0..=5.0,
))
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
});
if ui
.checkbox(
&mut settings.config.shader_hot_reload,
"Enable shader hot reload",
)
.on_hover_text("Automatically reload shaders when files change on disk")
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
if settings.config.shader_hot_reload {
ui.horizontal(|ui| {
ui.label("Hot reload delay:");
let mut delay = settings.config.shader_hot_reload_delay as u32;
if ui
.add(
egui::Slider::new(&mut delay, 50..=1000)
.suffix(" ms"),
)
.on_hover_text("Debounce delay before reloading shader after file change (helps avoid multiple reloads)")
.changed()
{
settings.config.shader_hot_reload_delay = delay as u64;
settings.has_changes = true;
*changes_this_frame = true;
}
});
}
ui.horizontal(|ui| {
ui.label("Shader brightness:");
if ui
.add(
egui::Slider::new(
&mut settings.config.shader.custom_shader_brightness,
0.05..=1.0,
)
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)),
)
.on_hover_text("Dim the shader background to improve text readability")
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
});
ui.horizontal(|ui| {
ui.label("Shader text opacity:");
if ui
.add(egui::Slider::new(
&mut settings.config.shader.custom_shader_text_opacity,
0.0..=1.0,
))
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
});
if ui
.checkbox(
&mut settings.config.shader.custom_shader_auto_dim_under_text,
"Auto-dim under text",
)
.on_hover_text(
"Sample terminal content and reduce shader intensity only beneath text/content pixels.",
)
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
if settings.config.shader.custom_shader_auto_dim_under_text {
ui.horizontal(|ui| {
ui.label("Auto-dim strength:");
if ui
.add(egui::Slider::new(
&mut settings.config.shader.custom_shader_auto_dim_strength,
0.0..=1.0,
))
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
});
}
ui.horizontal(|ui| {
if ui
.button("Cycle shader")
.on_hover_text("Action: cycle_background_shader")
.clicked()
{
let shaders = settings.background_shaders();
if !shaders.is_empty() {
let next = shaders
.iter()
.position(|shader| shader == &settings.temp_custom_shader)
.map(|index| (index + 1) % shaders.len())
.unwrap_or(0);
settings.temp_custom_shader = shaders[next].clone();
settings.config.shader.custom_shader = Some(shaders[next].clone());
settings.has_changes = true;
*changes_this_frame = true;
}
}
if ui
.button(if settings.config.shader.custom_shader_animation {
"Pause animation"
} else {
"Resume animation"
})
.on_hover_text("Action: toggle_shader_animation")
.clicked()
{
settings.config.shader.custom_shader_animation =
!settings.config.shader.custom_shader_animation;
settings.has_changes = true;
*changes_this_frame = true;
}
if ui
.checkbox(
&mut settings.config.shader.custom_shader_readability_mode,
"Readability/low-power mode",
)
.on_hover_text("Action: toggle_shader_readability_mode")
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
});
if settings.config.shader.custom_shader_readability_mode {
ui.horizontal(|ui| {
ui.label("Readability brightness cap:");
if ui
.add(egui::Slider::new(
&mut settings.config.shader.custom_shader_readability_brightness,
0.05..=1.0,
))
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
});
}
if ui
.checkbox(
&mut settings.config.shader.custom_shader_full_content,
"Full content mode",
)
.on_hover_text("When enabled, shader receives and can manipulate the full terminal content (text + background). When disabled, shader only provides background and text is composited cleanly on top.")
.changed()
{
settings.has_changes = true;
*changes_this_frame = true;
}
ui.add_space(8.0);
show_cubemap_controls(ui, settings, changes_this_frame);
let has_shader_path = !settings.temp_custom_shader.is_empty();
ui.horizontal(|ui| {
let edit_button = ui.add_enabled(has_shader_path, egui::Button::new("Edit Shader..."));
if edit_button.clicked() {
let shader_path = par_term_config::Config::shader_path(&settings.temp_custom_shader);
match std::fs::read_to_string(&shader_path) {
Ok(source) => {
settings.shader_editor_source = source.clone();
settings.shader_editor_original = source;
settings.shader_editor_error = None;
settings.shader_editor_visible = true;
}
Err(e) => {
settings.shader_editor_error = Some(format!(
"Failed to read shader file '{}': {}",
shader_path.display(),
e
));
}
}
}
if !has_shader_path {
ui.label("(set shader path first)");
}
});
ui.add_space(8.0);
show_global_channel_textures(ui, settings, changes_this_frame);
ui.add_space(8.0);
show_background_channel0_controls(ui, settings, changes_this_frame);
}