Skip to main content

par_term/
shader_install_ui.rs

1//! Shader installation prompt UI.
2//!
3//! Displays a dialog on first startup when the shaders folder is missing or empty,
4//! offering to download and install the shader pack from GitHub releases.
5
6use crate::shader_installer;
7use egui::{Align2, Color32, Context, Frame, RichText, Window, epaint::Shadow};
8
9/// User's response to the shader install prompt
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ShaderInstallResponse {
12    /// User clicked "Yes, Install" - download and install shaders
13    Install,
14    /// User clicked "Never" - save preference to config
15    Never,
16    /// User clicked "Later" - dismiss for this session only
17    Later,
18    /// No response yet (dialog still showing or not shown)
19    None,
20}
21
22/// Shader install dialog UI manager
23pub struct ShaderInstallUI {
24    /// Whether the dialog is currently visible
25    pub visible: bool,
26    /// Whether installation is in progress
27    pub installing: bool,
28    /// Installation progress message
29    pub progress_message: Option<String>,
30    /// Installation error message
31    pub error_message: Option<String>,
32    /// Installation success message
33    pub success_message: Option<String>,
34}
35
36impl ShaderInstallUI {
37    /// Create a new shader install UI
38    pub fn new() -> Self {
39        Self {
40            visible: false,
41            installing: false,
42            progress_message: None,
43            error_message: None,
44            success_message: None,
45        }
46    }
47
48    /// Show the dialog
49    pub fn show_dialog(&mut self) {
50        self.visible = true;
51        self.installing = false;
52        self.progress_message = None;
53        self.error_message = None;
54        self.success_message = None;
55    }
56
57    /// Hide the dialog
58    pub fn hide(&mut self) {
59        self.visible = false;
60    }
61
62    /// Render the shader install dialog
63    /// Returns the user's response
64    pub fn show(&mut self, ctx: &Context) -> ShaderInstallResponse {
65        if !self.visible {
66            return ShaderInstallResponse::None;
67        }
68
69        let mut response = ShaderInstallResponse::None;
70
71        // Ensure dialog is fully opaque
72        let mut style = (*ctx.style()).clone();
73        let solid_bg = Color32::from_rgba_unmultiplied(32, 32, 32, 255);
74        style.visuals.window_fill = solid_bg;
75        style.visuals.panel_fill = solid_bg;
76        style.visuals.widgets.noninteractive.bg_fill = solid_bg;
77        ctx.set_style(style);
78
79        let viewport = ctx.input(|i| i.viewport_rect());
80
81        Window::new("Shader Pack Available")
82            .resizable(false)
83            .collapsible(false)
84            .default_width(450.0)
85            .default_pos(viewport.center())
86            .pivot(Align2::CENTER_CENTER)
87            .frame(
88                Frame::window(&ctx.style())
89                    .fill(solid_bg)
90                    .inner_margin(20.0)
91                    .stroke(egui::Stroke::new(1.0, Color32::from_gray(80)))
92                    .shadow(Shadow {
93                        offset: [4, 4],
94                        blur: 16,
95                        spread: 4,
96                        color: Color32::from_black_alpha(180),
97                    }),
98            )
99            .show(ctx, |ui| {
100                ui.vertical_centered(|ui| {
101                    // Header with icon
102                    ui.add_space(8.0);
103                    ui.label(
104                        RichText::new("Custom Shaders Available")
105                            .size(20.0)
106                            .strong(),
107                    );
108                    ui.add_space(16.0);
109                });
110
111                // Description
112                ui.label(
113                    "par-term includes 49+ custom background shaders and 12 cursor \
114                     effect shaders that can transform your terminal experience.",
115                );
116                ui.add_space(8.0);
117
118                ui.label("Effects include:");
119                ui.indent("effects_list", |ui| {
120                    ui.label("- CRT monitors, scanlines, and retro effects");
121                    ui.label("- Matrix rain, starfields, and particle systems");
122                    ui.label("- Plasma, fire, and abstract visualizations");
123                    ui.label("- Cursor trails, glows, and ripple effects");
124                });
125
126                ui.add_space(16.0);
127
128                // Show installation progress/error/success
129                if self.installing {
130                    ui.horizontal(|ui| {
131                        ui.spinner();
132                        ui.label(
133                            self.progress_message
134                                .as_deref()
135                                .unwrap_or("Installing shaders..."),
136                        );
137                    });
138                } else if let Some(error) = &self.error_message {
139                    ui.colored_label(Color32::from_rgb(255, 100, 100), error);
140                    ui.add_space(8.0);
141                    ui.label("You can try again later using: par-term install-shaders");
142                } else if let Some(success) = &self.success_message {
143                    ui.colored_label(Color32::from_rgb(100, 255, 100), success);
144                    ui.add_space(8.0);
145                    ui.label("Configure shaders in Settings (F12) under 'Background & Effects'.");
146                }
147
148                ui.add_space(16.0);
149
150                // Buttons (centered)
151                ui.vertical_centered(|ui| {
152                    // Don't show buttons during installation or after success
153                    if !self.installing && self.success_message.is_none() {
154                        ui.horizontal(|ui| {
155                            // Calculate button width for uniform sizing
156                            let button_width = 120.0;
157
158                            if ui
159                                .add_sized([button_width, 32.0], egui::Button::new("Yes, Install"))
160                                .clicked()
161                            {
162                                response = ShaderInstallResponse::Install;
163                            }
164
165                            ui.add_space(8.0);
166
167                            if ui
168                                .add_sized([button_width, 32.0], egui::Button::new("Never"))
169                                .on_hover_text("Don't ask again")
170                                .clicked()
171                            {
172                                response = ShaderInstallResponse::Never;
173                            }
174
175                            ui.add_space(8.0);
176
177                            if ui
178                                .add_sized([button_width, 32.0], egui::Button::new("Later"))
179                                .on_hover_text("Ask again next time")
180                                .clicked()
181                            {
182                                response = ShaderInstallResponse::Later;
183                            }
184                        });
185                    } else if self.success_message.is_some() {
186                        // Show OK button after successful install
187                        if ui
188                            .add_sized([120.0, 32.0], egui::Button::new("OK"))
189                            .clicked()
190                        {
191                            self.visible = false;
192                        }
193                    }
194                });
195
196                ui.add_space(8.0);
197
198                // Help text
199                if !self.installing
200                    && self.success_message.is_none()
201                    && self.error_message.is_none()
202                {
203                    ui.vertical_centered(|ui| {
204                        ui.label(
205                            RichText::new(
206                                "You can always install later with: par-term install-shaders",
207                            )
208                            .weak()
209                            .small(),
210                        );
211                    });
212                }
213            });
214
215        response
216    }
217
218    /// Set installation in progress
219    pub fn set_installing(&mut self, message: &str) {
220        self.installing = true;
221        self.progress_message = Some(message.to_string());
222        self.error_message = None;
223    }
224
225    /// Set installation error
226    pub fn set_error(&mut self, error: &str) {
227        self.installing = false;
228        self.progress_message = None;
229        self.error_message = Some(error.to_string());
230    }
231
232    /// Set installation success
233    pub fn set_success(&mut self, message: &str) {
234        self.installing = false;
235        self.progress_message = None;
236        self.error_message = None;
237        self.success_message = Some(message.to_string());
238    }
239}
240
241impl Default for ShaderInstallUI {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247/// Install shaders from GitHub release (delegates to shared shader_installer module)
248/// This is a blocking operation that downloads and extracts shaders
249pub fn install_shaders_headless() -> Result<usize, String> {
250    shader_installer::install_shaders()
251}