Skip to main content

par_term/settings_window/
mod.rs

1//! Separate settings window for the terminal emulator.
2//!
3//! This module provides a standalone window for the settings UI,
4//! allowing users to configure the terminal while viewing terminal content.
5
6mod render;
7
8use crate::settings_ui::SettingsUI;
9use anyhow::{Context, Result};
10use par_term_config::Config;
11
12// Re-export SettingsWindowAction so the rest of the crate can use it via
13// `crate::settings_window::SettingsWindowAction` as before.
14pub use crate::settings_ui::SettingsWindowAction;
15use std::sync::Arc;
16use winit::event::WindowEvent;
17use winit::event_loop::ActiveEventLoop;
18use winit::keyboard::{Key, NamedKey};
19use winit::window::{Window, WindowId};
20
21/// Manages a separate settings window with its own egui context and wgpu renderer
22pub struct SettingsWindow {
23    /// The winit window
24    pub(super) window: Arc<Window>,
25    /// Window ID for event routing
26    window_id: WindowId,
27    /// wgpu surface
28    pub(super) surface: wgpu::Surface<'static>,
29    /// wgpu device
30    pub(super) device: Arc<wgpu::Device>,
31    /// wgpu queue
32    pub(super) queue: Arc<wgpu::Queue>,
33    /// Surface configuration
34    pub(super) surface_config: wgpu::SurfaceConfiguration,
35    /// egui context
36    pub(super) egui_ctx: egui::Context,
37    /// egui-winit state
38    pub(super) egui_state: egui_winit::State,
39    /// egui-wgpu renderer
40    pub(super) egui_renderer: egui_wgpu::Renderer,
41    /// Settings UI component
42    pub settings_ui: SettingsUI,
43    /// Whether the window is ready for rendering
44    pub(super) ready: bool,
45    /// Flag to indicate window should close
46    should_close: bool,
47    /// Pending paste text from menu accelerator (injected into egui next frame)
48    pub(super) pending_paste: Option<String>,
49    /// Pending egui events from menu accelerators (Copy, Cut, SelectAll)
50    pub(super) pending_events: Vec<egui::Event>,
51}
52
53impl SettingsWindow {
54    /// Create a new settings window
55    pub async fn new(
56        event_loop: &ActiveEventLoop,
57        config: Config,
58        supported_vsync_modes: Vec<crate::config::VsyncMode>,
59    ) -> Result<Self> {
60        // Create the window
61        let window_attrs = Window::default_attributes()
62            .with_title("Settings")
63            .with_inner_size(winit::dpi::LogicalSize::new(770, 800))
64            .with_min_inner_size(winit::dpi::LogicalSize::new(550, 400))
65            .with_resizable(true);
66
67        let window = Arc::new(event_loop.create_window(window_attrs)?);
68        let window_id = window.id();
69        let size = window.inner_size();
70
71        // Create wgpu instance
72        // Platform-specific backend selection for better VM compatibility
73        #[cfg(target_os = "windows")]
74        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
75            backends: wgpu::Backends::DX12,
76            ..wgpu::InstanceDescriptor::new_without_display_handle()
77        });
78        #[cfg(target_os = "macos")]
79        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
80            backends: wgpu::Backends::all(),
81            ..wgpu::InstanceDescriptor::new_without_display_handle()
82        });
83        #[cfg(target_os = "linux")]
84        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
85            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
86            ..wgpu::InstanceDescriptor::new_without_display_handle()
87        });
88
89        // Create surface
90        let surface = instance.create_surface(window.clone())?;
91
92        // Request adapter
93        let adapter = instance
94            .request_adapter(&wgpu::RequestAdapterOptions {
95                power_preference: wgpu::PowerPreference::LowPower,
96                compatible_surface: Some(&surface),
97                force_fallback_adapter: false,
98            })
99            .await
100            .context("Failed to find suitable GPU adapter")?;
101
102        // Request device
103        let (device, queue) = adapter
104            .request_device(&wgpu::DeviceDescriptor::default())
105            .await?;
106
107        let device = Arc::new(device);
108        let queue = Arc::new(queue);
109
110        // Configure surface
111        let surface_caps = surface.get_capabilities(&adapter);
112        let surface_format = surface_caps
113            .formats
114            .iter()
115            .find(|f| f.is_srgb())
116            .copied()
117            .or_else(|| surface_caps.formats.first().copied())
118            .context("Surface reports no supported texture formats")?;
119
120        // Select alpha mode for window transparency (consistent with main window)
121        let alpha_mode = if surface_caps
122            .alpha_modes
123            .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
124        {
125            wgpu::CompositeAlphaMode::PreMultiplied
126        } else if surface_caps
127            .alpha_modes
128            .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
129        {
130            wgpu::CompositeAlphaMode::PostMultiplied
131        } else if surface_caps
132            .alpha_modes
133            .contains(&wgpu::CompositeAlphaMode::Auto)
134        {
135            wgpu::CompositeAlphaMode::Auto
136        } else {
137            surface_caps
138                .alpha_modes
139                .first()
140                .copied()
141                .context("Surface reports no supported alpha modes")?
142        };
143
144        let surface_config = wgpu::SurfaceConfiguration {
145            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
146            format: surface_format,
147            width: size.width.max(1),
148            height: size.height.max(1),
149            present_mode: wgpu::PresentMode::AutoVsync,
150            alpha_mode,
151            view_formats: vec![],
152            desired_maximum_frame_latency: 2,
153        };
154        surface.configure(&device, &surface_config);
155
156        // Initialize egui
157        let scale_factor = window.scale_factor() as f32;
158        let egui_ctx = egui::Context::default();
159        crate::settings_ui::nerd_font::configure_nerd_font(&egui_ctx);
160        let egui_state = egui_winit::State::new(
161            egui_ctx.clone(),
162            egui::ViewportId::ROOT,
163            &window,
164            Some(scale_factor),
165            None,
166            None,
167        );
168
169        // Create egui renderer
170        let egui_renderer = egui_wgpu::Renderer::new(
171            &device,
172            surface_format,
173            egui_wgpu::RendererOptions {
174                msaa_samples: 1,
175                depth_stencil_format: None,
176                dithering: false,
177                predictable_texture_filtering: false,
178            },
179        );
180
181        // Create settings UI
182        let mut settings_ui = SettingsUI::new(config);
183        settings_ui.visible = true; // Always visible in settings window
184        settings_ui.update_supported_vsync_modes(supported_vsync_modes);
185
186        Ok(Self {
187            window,
188            window_id,
189            surface,
190            device,
191            queue,
192            surface_config,
193            egui_ctx,
194            egui_state,
195            egui_renderer,
196            settings_ui,
197            ready: true,
198            should_close: false,
199            pending_paste: None,
200            pending_events: Vec::new(),
201        })
202    }
203
204    /// Get the window ID
205    pub fn window_id(&self) -> WindowId {
206        self.window_id
207    }
208
209    /// Check if the window should close
210    pub fn should_close(&self) -> bool {
211        self.should_close
212    }
213
214    /// Queue a paste event for the next egui frame.
215    ///
216    /// Used when the macOS menu accelerator intercepts Cmd+V before
217    /// the keypress reaches egui.
218    pub fn inject_paste(&mut self, text: String) {
219        self.pending_paste = Some(text);
220        self.window.request_redraw();
221    }
222
223    /// Queue an egui event for the next frame.
224    ///
225    /// Used when menu accelerators intercept Cmd+C, Cmd+X, Cmd+A, etc.
226    /// before egui sees them.
227    pub fn inject_event(&mut self, event: egui::Event) {
228        self.pending_events.push(event);
229        self.window.request_redraw();
230    }
231
232    /// Update the config in the settings UI
233    pub fn update_config(&mut self, config: Config) {
234        self.settings_ui.update_config(config);
235    }
236
237    /// Force-update the config, bypassing the `has_changes` guard.
238    /// Used when the ACP agent changes config — must propagate even if
239    /// the user has pending edits in the settings window.
240    pub fn force_update_config(&mut self, config: Config) {
241        self.settings_ui.force_update_config(config);
242    }
243
244    /// Set a shader compilation error message
245    pub fn set_shader_error(&mut self, error: Option<String>) {
246        self.settings_ui.set_shader_error(error);
247    }
248
249    /// Set a cursor shader compilation error message
250    pub fn set_cursor_shader_error(&mut self, error: Option<String>) {
251        self.settings_ui.set_cursor_shader_error(error);
252    }
253
254    /// Clear shader error
255    pub fn clear_shader_error(&mut self) {
256        self.settings_ui.clear_shader_error();
257    }
258
259    /// Clear cursor shader error
260    pub fn clear_cursor_shader_error(&mut self) {
261        self.settings_ui.clear_cursor_shader_error();
262    }
263
264    /// Sync shader enabled states from external source (e.g., keybinding toggle)
265    /// This prevents the settings window from overwriting externally toggled states
266    pub fn sync_shader_states(&mut self, custom_shader_enabled: bool, cursor_shader_enabled: bool) {
267        self.settings_ui.config.shader.custom_shader_enabled = custom_shader_enabled;
268        self.settings_ui.config.shader.cursor_shader_enabled = cursor_shader_enabled;
269    }
270
271    /// Handle a window event
272    pub fn handle_window_event(&mut self, event: WindowEvent) -> SettingsWindowAction {
273        // Let egui handle the event
274        let event_response = self.egui_state.on_window_event(&self.window, &event);
275
276        match event {
277            WindowEvent::CloseRequested => {
278                self.should_close = true;
279                return SettingsWindowAction::Close;
280            }
281
282            WindowEvent::Resized(new_size)
283                if new_size.width > 0 && new_size.height > 0 => {
284                    self.surface_config.width = new_size.width;
285                    self.surface_config.height = new_size.height;
286                    self.surface.configure(&self.device, &self.surface_config);
287                    self.window.request_redraw();
288                }
289
290            WindowEvent::KeyboardInput { event, .. }
291                // Handle Escape to close window (if egui didn't consume it)
292                if !event_response.consumed
293                    && event.state.is_pressed()
294                    && matches!(event.logical_key, Key::Named(NamedKey::Escape))
295                    // Only close if no shader editor is open
296                    && !self.settings_ui.shader_editor_visible
297                        && !self.settings_ui.cursor_shader_editor_visible
298                    => {
299                        self.should_close = true;
300                        return SettingsWindowAction::Close;
301                    }
302
303            WindowEvent::RedrawRequested => {
304                return self.render();
305            }
306
307            _ => {}
308        }
309
310        // Request redraw if egui needs it
311        if event_response.repaint {
312            self.window.request_redraw();
313        }
314
315        SettingsWindowAction::None
316    }
317
318    /// Request a redraw
319    pub fn request_redraw(&self) {
320        self.window.request_redraw();
321    }
322
323    /// Bring the window to the front and focus it
324    pub fn focus(&self) {
325        self.window.focus_window();
326        self.window.request_redraw();
327    }
328
329    /// Check if the settings window currently has focus.
330    pub fn is_focused(&self) -> bool {
331        self.window.has_focus()
332    }
333}