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