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                apply_limit_buckets: false,
99            })
100            .await
101            .context("Failed to find suitable GPU adapter")?;
102
103        // Request device
104        let (device, queue) = adapter
105            .request_device(&wgpu::DeviceDescriptor {
106                required_limits: par_term_render::texture_limits(
107                    adapter.limits().max_texture_dimension_2d,
108                ),
109                ..Default::default()
110            })
111            .await?;
112        par_term_render::install_nonfatal_error_handler(&device);
113
114        let device = Arc::new(device);
115        let queue = Arc::new(queue);
116
117        // Configure surface
118        let surface_caps = surface.get_capabilities(&adapter);
119        let surface_format = surface_caps
120            .formats
121            .iter()
122            .find(|f| f.is_srgb())
123            .copied()
124            .or_else(|| surface_caps.formats.first().copied())
125            .context("Surface reports no supported texture formats")?;
126
127        // Select alpha mode for window transparency (consistent with main window)
128        let alpha_mode = if surface_caps
129            .alpha_modes
130            .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
131        {
132            wgpu::CompositeAlphaMode::PreMultiplied
133        } else if surface_caps
134            .alpha_modes
135            .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
136        {
137            wgpu::CompositeAlphaMode::PostMultiplied
138        } else if surface_caps
139            .alpha_modes
140            .contains(&wgpu::CompositeAlphaMode::Auto)
141        {
142            wgpu::CompositeAlphaMode::Auto
143        } else {
144            surface_caps
145                .alpha_modes
146                .first()
147                .copied()
148                .context("Surface reports no supported alpha modes")?
149        };
150
151        let (surface_width, surface_height) = par_term_render::clamp_surface_extent(
152            size.width,
153            size.height,
154            device.limits().max_texture_dimension_2d,
155        );
156        let surface_config = wgpu::SurfaceConfiguration {
157            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
158            format: surface_format,
159            color_space: wgpu::SurfaceColorSpace::Auto,
160            width: surface_width,
161            height: surface_height,
162            present_mode: wgpu::PresentMode::AutoVsync,
163            alpha_mode,
164            view_formats: vec![],
165            desired_maximum_frame_latency: 2,
166        };
167        surface.configure(&device, &surface_config);
168
169        // Initialize egui
170        let scale_factor = window.scale_factor() as f32;
171        let egui_ctx = egui::Context::default();
172        crate::settings_ui::nerd_font::configure_nerd_font(&egui_ctx);
173        let egui_state = egui_winit::State::new(
174            egui_ctx.clone(),
175            egui::ViewportId::ROOT,
176            &window,
177            Some(scale_factor),
178            None,
179            None,
180        );
181
182        // Create egui renderer
183        let egui_renderer = egui_wgpu::Renderer::new(
184            &device,
185            surface_format,
186            egui_wgpu::RendererOptions {
187                msaa_samples: 1,
188                depth_stencil_format: None,
189                dithering: false,
190                predictable_texture_filtering: false,
191            },
192        );
193
194        // Create settings UI
195        let mut settings_ui = SettingsUI::new(config);
196        settings_ui.visible = true; // Always visible in settings window
197        settings_ui.update_supported_vsync_modes(supported_vsync_modes);
198
199        Ok(Self {
200            window,
201            window_id,
202            surface,
203            device,
204            queue,
205            surface_config,
206            egui_ctx,
207            egui_state,
208            egui_renderer,
209            settings_ui,
210            ready: true,
211            should_close: false,
212            pending_paste: None,
213            pending_events: Vec::new(),
214        })
215    }
216
217    /// Get the window ID
218    pub fn window_id(&self) -> WindowId {
219        self.window_id
220    }
221
222    /// Check if the window should close
223    pub fn should_close(&self) -> bool {
224        self.should_close
225    }
226
227    /// Queue a paste event for the next egui frame.
228    ///
229    /// Used when the macOS menu accelerator intercepts Cmd+V before
230    /// the keypress reaches egui.
231    pub fn inject_paste(&mut self, text: String) {
232        self.pending_paste = Some(text);
233        self.window.request_redraw();
234    }
235
236    /// Queue an egui event for the next frame.
237    ///
238    /// Used when menu accelerators intercept Cmd+C, Cmd+X, Cmd+A, etc.
239    /// before egui sees them.
240    pub fn inject_event(&mut self, event: egui::Event) {
241        self.pending_events.push(event);
242        self.window.request_redraw();
243    }
244
245    /// Update the config in the settings UI
246    pub fn update_config(&mut self, config: Config) {
247        self.settings_ui.update_config(config);
248    }
249
250    /// Force-update the config, bypassing the `has_changes` guard.
251    /// Used when the ACP agent changes config — must propagate even if
252    /// the user has pending edits in the settings window.
253    pub fn force_update_config(&mut self, config: Config) {
254        self.settings_ui.force_update_config(config);
255    }
256
257    /// Set a shader compilation error message
258    pub fn set_shader_error(&mut self, error: Option<String>) {
259        self.settings_ui.set_shader_error(error);
260    }
261
262    /// Set a cursor shader compilation error message
263    pub fn set_cursor_shader_error(&mut self, error: Option<String>) {
264        self.settings_ui.set_cursor_shader_error(error);
265    }
266
267    /// Clear shader error
268    pub fn clear_shader_error(&mut self) {
269        self.settings_ui.clear_shader_error();
270    }
271
272    /// Clear cursor shader error
273    pub fn clear_cursor_shader_error(&mut self) {
274        self.settings_ui.clear_cursor_shader_error();
275    }
276
277    /// Sync shader enabled states from external source (e.g., keybinding toggle)
278    /// This prevents the settings window from overwriting externally toggled states
279    pub fn sync_shader_states(&mut self, custom_shader_enabled: bool, cursor_shader_enabled: bool) {
280        self.settings_ui.config.shader.custom_shader_enabled = custom_shader_enabled;
281        self.settings_ui.config.shader.cursor_shader_enabled = cursor_shader_enabled;
282    }
283
284    /// Handle a window event
285    pub fn handle_window_event(&mut self, event: WindowEvent) -> SettingsWindowAction {
286        // Let egui handle the event
287        let event_response = self.egui_state.on_window_event(&self.window, &event);
288
289        match event {
290            WindowEvent::CloseRequested => {
291                self.should_close = true;
292                return SettingsWindowAction::Close;
293            }
294
295            WindowEvent::Resized(new_size)
296                if new_size.width > 0 && new_size.height > 0 => {
297                    let (width, height) = par_term_render::clamp_surface_extent(
298                        new_size.width,
299                        new_size.height,
300                        self.device.limits().max_texture_dimension_2d,
301                    );
302                    self.surface_config.width = width;
303                    self.surface_config.height = height;
304                    self.surface.configure(&self.device, &self.surface_config);
305                    self.window.request_redraw();
306                }
307
308            WindowEvent::KeyboardInput { event, .. }
309                // Handle Escape to close window (if egui didn't consume it)
310                if !event_response.consumed
311                    && event.state.is_pressed()
312                    && matches!(event.logical_key, Key::Named(NamedKey::Escape))
313                    // Only close if no shader editor is open
314                    && !self.settings_ui.shader_editor_visible
315                        && !self.settings_ui.cursor_shader_editor_visible
316                    => {
317                        self.should_close = true;
318                        return SettingsWindowAction::Close;
319                    }
320
321            WindowEvent::RedrawRequested => {
322                return self.render();
323            }
324
325            _ => {}
326        }
327
328        // Request redraw if egui needs it
329        if event_response.repaint {
330            self.window.request_redraw();
331        }
332
333        SettingsWindowAction::None
334    }
335
336    /// Request a redraw
337    pub fn request_redraw(&self) {
338        self.window.request_redraw();
339    }
340
341    /// Bring the window to the front and focus it
342    pub fn focus(&self) {
343        self.window.focus_window();
344        self.window.request_redraw();
345    }
346
347    /// Check if the settings window currently has focus.
348    pub fn is_focused(&self) -> bool {
349        self.window.has_focus()
350    }
351}