par_term/app/
window_manager.rs1use 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
15pub struct WindowManager {
17 pub(crate) windows: HashMap<WindowId, WindowState>,
19 pub(crate) menu: Option<MenuManager>,
21 pub(crate) config: Config,
23 pub(crate) runtime: Arc<Runtime>,
25 pub(crate) should_exit: bool,
27 pending_window_count: usize,
29}
30
31impl WindowManager {
32 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 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 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 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 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 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 if self.menu.is_none() {
99 match MenuManager::new() {
100 Ok(menu) => {
101 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 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 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 drop(window_state);
145 }
146
147 if self.windows.is_empty() {
149 log::info!("Last window closed, exiting application");
150 self.should_exit = true;
151 }
152 }
153
154 #[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 #[allow(dead_code)]
162 pub fn get_window(&self, window_id: WindowId) -> Option<&WindowState> {
163 self.windows.get(&window_id)
164 }
165
166 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 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 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 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 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 }
398 }
399 }
400
401 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 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}