par_term/app/
window_manager.rs

1//! Multi-window manager for the terminal emulator
2//!
3//! This module contains `WindowManager`, which coordinates multiple terminal windows,
4//! handles the native menu system, and manages shared resources.
5
6use crate::app::window_state::WindowState;
7use crate::config::Config;
8use crate::menu::{MenuAction, MenuManager};
9use std::collections::HashMap;
10use std::sync::Arc;
11use tokio::runtime::Runtime;
12use winit::event_loop::ActiveEventLoop;
13use winit::window::WindowId;
14
15/// Manages multiple terminal windows and shared resources
16pub struct WindowManager {
17    /// Per-window state indexed by window ID
18    pub(crate) windows: HashMap<WindowId, WindowState>,
19    /// Native menu manager
20    pub(crate) menu: Option<MenuManager>,
21    /// Shared configuration (read at startup, each window gets a clone)
22    pub(crate) config: Config,
23    /// Shared async runtime
24    pub(crate) runtime: Arc<Runtime>,
25    /// Flag to indicate if app should exit
26    pub(crate) should_exit: bool,
27    /// Counter for generating unique window IDs during creation
28    pending_window_count: usize,
29}
30
31impl WindowManager {
32    /// Create a new window manager
33    pub fn new(config: Config, runtime: Arc<Runtime>) -> Self {
34        Self {
35            windows: HashMap::new(),
36            menu: None,
37            config,
38            runtime,
39            should_exit: false,
40            pending_window_count: 0,
41        }
42    }
43
44    /// Create a new window with a fresh terminal session
45    pub fn create_window(&mut self, event_loop: &ActiveEventLoop) {
46        use winit::window::Window;
47
48        let mut window_attrs = Window::default_attributes()
49            .with_title(&self.config.window_title)
50            .with_inner_size(winit::dpi::LogicalSize::new(
51                self.config.window_width,
52                self.config.window_height,
53            ))
54            .with_decorations(self.config.window_decorations);
55
56        // Load and set the application icon
57        let icon_bytes = include_bytes!("../../assets/icon.png");
58        if let Ok(icon_image) = image::load_from_memory(icon_bytes) {
59            let rgba = icon_image.to_rgba8();
60            let (width, height) = rgba.dimensions();
61            if let Ok(icon) = winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
62                window_attrs = window_attrs.with_window_icon(Some(icon));
63                log::info!("Window icon set ({}x{})", width, height);
64            } else {
65                log::warn!("Failed to create window icon from RGBA data");
66            }
67        } else {
68            log::warn!("Failed to load embedded icon image");
69        }
70
71        // Set window always-on-top if requested
72        if self.config.window_always_on_top {
73            window_attrs = window_attrs.with_window_level(winit::window::WindowLevel::AlwaysOnTop);
74            log::info!("Window always-on-top enabled");
75        }
76
77        // Always enable window transparency support for runtime opacity changes
78        window_attrs = window_attrs.with_transparent(true);
79        log::info!(
80            "Window transparency enabled (opacity: {})",
81            self.config.window_opacity
82        );
83
84        match event_loop.create_window(window_attrs) {
85            Ok(window) => {
86                let window_id = window.id();
87                let mut window_state =
88                    WindowState::new(self.config.clone(), Arc::clone(&self.runtime));
89
90                // Initialize async components using the shared runtime
91                let runtime = Arc::clone(&self.runtime);
92                if let Err(e) = runtime.block_on(window_state.initialize_async(window)) {
93                    log::error!("Failed to initialize window: {}", e);
94                    return;
95                }
96
97                // Initialize menu for the first window (macOS global menu) or per-window (Windows/Linux)
98                if self.menu.is_none() {
99                    match MenuManager::new() {
100                        Ok(menu) => {
101                            // Attach menu to window (platform-specific)
102                            if let Some(win) = &window_state.window
103                                && let Err(e) = menu.init_for_window(win)
104                            {
105                                log::warn!("Failed to initialize menu for window: {}", e);
106                            }
107                            self.menu = Some(menu);
108                        }
109                        Err(e) => {
110                            log::warn!("Failed to create menu: {}", e);
111                        }
112                    }
113                } else if let Some(menu) = &self.menu
114                    && let Some(win) = &window_state.window
115                    && let Err(e) = menu.init_for_window(win)
116                {
117                    // For additional windows on Windows/Linux, attach menu
118                    log::warn!("Failed to initialize menu for window: {}", e);
119                }
120
121                self.windows.insert(window_id, window_state);
122                self.pending_window_count += 1;
123                log::info!(
124                    "Created new window {:?} (total: {})",
125                    window_id,
126                    self.windows.len()
127                );
128            }
129            Err(e) => {
130                log::error!("Failed to create window: {}", e);
131            }
132        }
133    }
134
135    /// Close a specific window
136    pub fn close_window(&mut self, window_id: WindowId) {
137        if let Some(window_state) = self.windows.remove(&window_id) {
138            log::info!(
139                "Closing window {:?} (remaining: {})",
140                window_id,
141                self.windows.len()
142            );
143            // WindowState's Drop impl handles cleanup
144            drop(window_state);
145        }
146
147        // Exit app when last window closes
148        if self.windows.is_empty() {
149            log::info!("Last window closed, exiting application");
150            self.should_exit = true;
151        }
152    }
153
154    /// Get mutable reference to a window's state
155    #[allow(dead_code)]
156    pub fn get_window_mut(&mut self, window_id: WindowId) -> Option<&mut WindowState> {
157        self.windows.get_mut(&window_id)
158    }
159
160    /// Get reference to a window's state
161    #[allow(dead_code)]
162    pub fn get_window(&self, window_id: WindowId) -> Option<&WindowState> {
163        self.windows.get(&window_id)
164    }
165
166    /// Handle a menu action
167    pub fn handle_menu_action(
168        &mut self,
169        action: MenuAction,
170        event_loop: &ActiveEventLoop,
171        focused_window: Option<WindowId>,
172    ) {
173        match action {
174            MenuAction::NewWindow => {
175                self.create_window(event_loop);
176            }
177            MenuAction::CloseWindow => {
178                if let Some(window_id) = focused_window {
179                    self.close_window(window_id);
180                }
181            }
182            MenuAction::NewTab => {
183                if let Some(window_id) = focused_window
184                    && let Some(window_state) = self.windows.get_mut(&window_id)
185                {
186                    window_state.new_tab();
187                }
188            }
189            MenuAction::CloseTab => {
190                if let Some(window_id) = focused_window
191                    && let Some(window_state) = self.windows.get_mut(&window_id)
192                    && window_state.close_current_tab()
193                {
194                    // Last tab closed, close the window
195                    self.close_window(window_id);
196                }
197            }
198            MenuAction::NextTab => {
199                if let Some(window_id) = focused_window
200                    && let Some(window_state) = self.windows.get_mut(&window_id)
201                {
202                    window_state.next_tab();
203                }
204            }
205            MenuAction::PreviousTab => {
206                if let Some(window_id) = focused_window
207                    && let Some(window_state) = self.windows.get_mut(&window_id)
208                {
209                    window_state.prev_tab();
210                }
211            }
212            MenuAction::SwitchToTab(index) => {
213                if let Some(window_id) = focused_window
214                    && let Some(window_state) = self.windows.get_mut(&window_id)
215                {
216                    window_state.switch_to_tab_index(index);
217                }
218            }
219            MenuAction::MoveTabLeft => {
220                if let Some(window_id) = focused_window
221                    && let Some(window_state) = self.windows.get_mut(&window_id)
222                {
223                    window_state.move_tab_left();
224                }
225            }
226            MenuAction::MoveTabRight => {
227                if let Some(window_id) = focused_window
228                    && let Some(window_state) = self.windows.get_mut(&window_id)
229                {
230                    window_state.move_tab_right();
231                }
232            }
233            MenuAction::DuplicateTab => {
234                if let Some(window_id) = focused_window
235                    && let Some(window_state) = self.windows.get_mut(&window_id)
236                {
237                    window_state.duplicate_tab();
238                }
239            }
240            MenuAction::Quit => {
241                // Close all windows
242                let window_ids: Vec<_> = self.windows.keys().copied().collect();
243                for window_id in window_ids {
244                    self.close_window(window_id);
245                }
246            }
247            MenuAction::Copy => {
248                if let Some(window_id) = focused_window
249                    && let Some(window_state) = self.windows.get_mut(&window_id)
250                    && let Some(text) = window_state.get_selected_text()
251                    && let Err(e) = window_state.input_handler.copy_to_clipboard(&text)
252                {
253                    log::error!("Failed to copy to clipboard: {}", e);
254                }
255            }
256            MenuAction::Paste => {
257                if let Some(window_id) = focused_window
258                    && let Some(window_state) = self.windows.get_mut(&window_id)
259                    && let Some(text) = window_state.input_handler.paste_from_clipboard()
260                    && let Ok(text_str) = std::str::from_utf8(&text)
261                {
262                    window_state.paste_text(text_str);
263                }
264            }
265            MenuAction::SelectAll => {
266                // Not implemented for terminal - would select all visible text
267                log::debug!("SelectAll menu action (not implemented for terminal)");
268            }
269            MenuAction::ClearScrollback => {
270                if let Some(window_id) = focused_window
271                    && let Some(window_state) = self.windows.get_mut(&window_id)
272                {
273                    // Clear scrollback in active tab
274                    let cleared = if let Some(tab) = window_state.tab_manager.active_tab() {
275                        if let Ok(term) = tab.terminal.try_lock() {
276                            term.clear_scrollback();
277                            true
278                        } else {
279                            false
280                        }
281                    } else {
282                        false
283                    };
284
285                    if cleared {
286                        if let Some(tab) = window_state.tab_manager.active_tab_mut() {
287                            tab.cache.scrollback_len = 0;
288                        }
289                        window_state.set_scroll_target(0);
290                        log::info!("Cleared scrollback buffer");
291                    }
292                }
293            }
294            MenuAction::ClipboardHistory => {
295                if let Some(window_id) = focused_window
296                    && let Some(window_state) = self.windows.get_mut(&window_id)
297                {
298                    window_state.clipboard_history_ui.toggle();
299                    window_state.needs_redraw = true;
300                }
301            }
302            MenuAction::ToggleFullscreen => {
303                if let Some(window_id) = focused_window
304                    && let Some(window_state) = self.windows.get_mut(&window_id)
305                    && let Some(window) = &window_state.window
306                {
307                    window_state.is_fullscreen = !window_state.is_fullscreen;
308                    if window_state.is_fullscreen {
309                        window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
310                    } else {
311                        window.set_fullscreen(None);
312                    }
313                }
314            }
315            MenuAction::IncreaseFontSize => {
316                if let Some(window_id) = focused_window
317                    && let Some(window_state) = self.windows.get_mut(&window_id)
318                {
319                    window_state.config.font_size = (window_state.config.font_size + 1.0).min(72.0);
320                    window_state.pending_font_rebuild = true;
321                    if let Some(window) = &window_state.window {
322                        window.request_redraw();
323                    }
324                }
325            }
326            MenuAction::DecreaseFontSize => {
327                if let Some(window_id) = focused_window
328                    && let Some(window_state) = self.windows.get_mut(&window_id)
329                {
330                    window_state.config.font_size = (window_state.config.font_size - 1.0).max(6.0);
331                    window_state.pending_font_rebuild = true;
332                    if let Some(window) = &window_state.window {
333                        window.request_redraw();
334                    }
335                }
336            }
337            MenuAction::ResetFontSize => {
338                if let Some(window_id) = focused_window
339                    && let Some(window_state) = self.windows.get_mut(&window_id)
340                {
341                    window_state.config.font_size = 14.0;
342                    window_state.pending_font_rebuild = true;
343                    if let Some(window) = &window_state.window {
344                        window.request_redraw();
345                    }
346                }
347            }
348            MenuAction::ToggleFpsOverlay => {
349                if let Some(window_id) = focused_window
350                    && let Some(window_state) = self.windows.get_mut(&window_id)
351                {
352                    window_state.debug.show_fps_overlay = !window_state.debug.show_fps_overlay;
353                    if let Some(window) = &window_state.window {
354                        window.request_redraw();
355                    }
356                }
357            }
358            MenuAction::OpenSettings => {
359                if let Some(window_id) = focused_window
360                    && let Some(window_state) = self.windows.get_mut(&window_id)
361                {
362                    window_state.settings_ui.toggle();
363                    if let Some(window) = &window_state.window {
364                        window.request_redraw();
365                    }
366                }
367            }
368            MenuAction::Minimize => {
369                if let Some(window_id) = focused_window
370                    && let Some(window_state) = self.windows.get(&window_id)
371                    && let Some(window) = &window_state.window
372                {
373                    window.set_minimized(true);
374                }
375            }
376            MenuAction::Zoom => {
377                if let Some(window_id) = focused_window
378                    && let Some(window_state) = self.windows.get(&window_id)
379                    && let Some(window) = &window_state.window
380                {
381                    window.set_maximized(!window.is_maximized());
382                }
383            }
384            MenuAction::ShowHelp => {
385                if let Some(window_id) = focused_window
386                    && let Some(window_state) = self.windows.get_mut(&window_id)
387                {
388                    window_state.help_ui.toggle();
389                    if let Some(window) = &window_state.window {
390                        window.request_redraw();
391                    }
392                }
393            }
394            MenuAction::About => {
395                log::info!("About par-term v{}", env!("CARGO_PKG_VERSION"));
396                // Could show an about dialog here
397            }
398        }
399    }
400
401    /// Process any pending menu events
402    pub fn process_menu_events(
403        &mut self,
404        event_loop: &ActiveEventLoop,
405        focused_window: Option<WindowId>,
406    ) {
407        if let Some(menu) = &self.menu {
408            // Collect actions to avoid borrow conflicts
409            let actions: Vec<_> = menu.poll_events().collect();
410            for action in actions {
411                self.handle_menu_action(action, event_loop, focused_window);
412            }
413        }
414    }
415}