Skip to main content

par_term/tab/
pane_ops.rs

1//! Tab split pane operations.
2//!
3//! Provides methods for creating, closing, navigating, and resizing split panes
4//! within a tab. Pane operations use `PaneManager` to manage the binary tree
5//! layout of panes.
6
7use crate::config::Config;
8use crate::pane::{NavigationDirection, PaneManager, SplitDirection};
9use crate::tab::Tab;
10use std::sync::{Arc, atomic::Ordering};
11use tokio::runtime::Runtime;
12
13struct SplitRequest {
14    focus_new: bool,
15    dpi_scale: f32,
16    initial_command: Option<(String, Vec<String>)>,
17    split_percent: u8,
18}
19
20impl Tab {
21    /// Check if this tab has multiple panes (split)
22    pub fn has_multiple_panes(&self) -> bool {
23        self.pane_manager
24            .as_ref()
25            .is_some_and(|pm| pm.has_multiple_panes())
26    }
27
28    /// Get the number of panes in this tab
29    pub fn pane_count(&self) -> usize {
30        self.pane_manager
31            .as_ref()
32            .map(|pm| pm.pane_count())
33            .unwrap_or(1)
34    }
35
36    /// Split the current pane horizontally (panes stacked vertically)
37    ///
38    /// Returns the new pane ID if successful.
39    /// `dpi_scale` converts logical pixel config values to physical pixels.
40    /// `focus_new` controls whether the new pane receives focus after splitting.
41    /// `initial_command` — when `Some((cmd, args))` the new pane runs that process directly.
42    pub fn split_horizontal(
43        &mut self,
44        focus_new: bool,
45        config: &Config,
46        runtime: Arc<Runtime>,
47        dpi_scale: f32,
48        initial_command: Option<(String, Vec<String>)>,
49        split_percent: u8,
50    ) -> anyhow::Result<Option<crate::pane::PaneId>> {
51        self.split(
52            SplitDirection::Horizontal,
53            config,
54            runtime,
55            SplitRequest {
56                focus_new,
57                dpi_scale,
58                initial_command,
59                split_percent,
60            },
61        )
62    }
63
64    /// Split the current pane vertically (panes side by side)
65    ///
66    /// Returns the new pane ID if successful.
67    /// `dpi_scale` converts logical pixel config values to physical pixels.
68    /// `focus_new` controls whether the new pane receives focus after splitting.
69    /// `initial_command` — when `Some((cmd, args))` the new pane runs that process directly.
70    pub fn split_vertical(
71        &mut self,
72        focus_new: bool,
73        config: &Config,
74        runtime: Arc<Runtime>,
75        dpi_scale: f32,
76        initial_command: Option<(String, Vec<String>)>,
77        split_percent: u8,
78    ) -> anyhow::Result<Option<crate::pane::PaneId>> {
79        self.split(
80            SplitDirection::Vertical,
81            config,
82            runtime,
83            SplitRequest {
84                focus_new,
85                dpi_scale,
86                initial_command,
87                split_percent,
88            },
89        )
90    }
91
92    /// Split the focused pane in the given direction.
93    /// `dpi_scale` is used to convert logical pixel config values to physical pixels.
94    /// `focus_new` controls whether the new pane receives focus after splitting.
95    /// `initial_command` — when `Some((cmd, args))` the new pane runs that process directly.
96    fn split(
97        &mut self,
98        direction: SplitDirection,
99        config: &Config,
100        runtime: Arc<Runtime>,
101        request: SplitRequest,
102    ) -> anyhow::Result<Option<crate::pane::PaneId>> {
103        // Check max panes limit
104        if config.panes.max_panes > 0 && self.pane_count() >= config.panes.max_panes {
105            log::warn!(
106                "Cannot split: max panes limit ({}) reached",
107                config.panes.max_panes
108            );
109            return Ok(None);
110        }
111
112        // Initialize pane manager and create initial pane if needed
113        let needs_initial_pane = self
114            .pane_manager
115            .as_ref()
116            .map(|pm| pm.pane_count() == 0)
117            .unwrap_or(true);
118
119        if needs_initial_pane {
120            // Create pane manager if it doesn't exist
121            if self.pane_manager.is_none() {
122                let mut pm = PaneManager::new();
123                // Scale from logical pixels (config) to physical pixels for layout
124                pm.set_divider_width(
125                    config.panes.pane_divider_width.unwrap_or(2.0) * request.dpi_scale,
126                );
127                pm.set_divider_hit_width(config.panes.pane_divider_hit_width * request.dpi_scale);
128                self.pane_manager = Some(pm);
129            }
130
131            // Create initial pane with size calculated for AFTER the split
132            // (since we know a split is about to happen)
133            if let Some(ref mut pm) = self.pane_manager {
134                pm.create_initial_pane_for_split(
135                    direction,
136                    config,
137                    Arc::clone(&runtime),
138                    self.working_directory.clone(),
139                )?;
140                log::info!(
141                    "Created PaneManager for tab {} with initial pane on first split",
142                    self.id
143                );
144            }
145        }
146
147        // Perform the split
148        if let Some(ref mut pm) = self.pane_manager {
149            let ratio = (request.split_percent.clamp(10, 90) as f32) / 100.0;
150            let new_pane_id = pm.split(
151                direction,
152                request.focus_new,
153                config,
154                Arc::clone(&runtime),
155                request.initial_command,
156                ratio,
157            )?;
158            if let Some(id) = new_pane_id {
159                log::info!("Split tab {} {:?}, new pane {}", self.id, direction, id);
160            }
161            Ok(new_pane_id)
162        } else {
163            Ok(None)
164        }
165    }
166
167    /// Close the focused pane
168    ///
169    /// Returns true if this was the last pane (tab should close)
170    pub fn close_focused_pane(&mut self) -> bool {
171        if let Some(ref mut pm) = self.pane_manager
172            && let Some(focused_id) = pm.focused_pane_id()
173        {
174            let is_last = pm.close_pane(focused_id);
175            if is_last {
176                // Last pane closed, clear the pane manager
177                self.pane_manager = None;
178            }
179            return is_last;
180        }
181        // No pane manager or no focused pane means single pane tab
182        true
183    }
184
185    /// Check for exited panes and close them
186    ///
187    /// Returns (closed_pane_ids, tab_should_close) where:
188    /// - `closed_pane_ids`: Vec of pane IDs that were closed
189    /// - `tab_should_close`: true if all panes have exited (tab should close)
190    pub fn close_exited_panes(&mut self) -> (Vec<crate::pane::PaneId>, bool) {
191        let mut closed_panes = Vec::new();
192
193        // Get IDs of panes whose shells have exited
194        let exited_pane_ids: Vec<crate::pane::PaneId> = if let Some(ref pm) = self.pane_manager {
195            let focused_id = pm.focused_pane_id();
196            pm.all_panes()
197                .iter()
198                .filter_map(|pane| {
199                    let is_running = pane.is_running();
200                    crate::debug_info!(
201                        "PANE_CHECK",
202                        "Pane {} running={} focused={} bounds=({:.0},{:.0} {:.0}x{:.0})",
203                        pane.id,
204                        is_running,
205                        focused_id == Some(pane.id),
206                        pane.bounds.x,
207                        pane.bounds.y,
208                        pane.bounds.width,
209                        pane.bounds.height
210                    );
211                    if !is_running { Some(pane.id) } else { None }
212                })
213                .collect()
214        } else {
215            Vec::new()
216        };
217
218        // Close each exited pane
219        if let Some(ref mut pm) = self.pane_manager {
220            for pane_id in exited_pane_ids {
221                crate::debug_info!("PANE_CLOSE", "Closing pane {} - shell exited", pane_id);
222                let is_last = pm.close_pane(pane_id);
223                closed_panes.push(pane_id);
224
225                if is_last {
226                    // Last pane closed, clear the pane manager
227                    self.pane_manager = None;
228                    return (closed_panes, true);
229                }
230            }
231        }
232
233        (closed_panes, false)
234    }
235
236    /// Get the pane manager if split panes are enabled
237    pub fn pane_manager(&self) -> Option<&PaneManager> {
238        self.pane_manager.as_ref()
239    }
240
241    /// Get mutable access to the pane manager
242    pub fn pane_manager_mut(&mut self) -> Option<&mut PaneManager> {
243        self.pane_manager.as_mut()
244    }
245
246    /// Initialize the pane manager if not already present
247    ///
248    /// This is used for tmux integration where we need to create the pane manager
249    /// before applying a layout.
250    pub fn init_pane_manager(&mut self) {
251        if self.pane_manager.is_none() {
252            self.pane_manager = Some(PaneManager::new());
253        }
254    }
255
256    /// Set the pane bounds and resize terminals
257    ///
258    /// This should be called before creating splits to ensure panes are sized correctly.
259    /// If the pane manager doesn't exist yet, this creates it with the bounds set.
260    pub fn set_pane_bounds(
261        &mut self,
262        bounds: crate::pane::PaneBounds,
263        cell_width: f32,
264        cell_height: f32,
265    ) {
266        self.set_pane_bounds_with_padding(bounds, cell_width, cell_height, 0.0);
267    }
268
269    /// Set the pane bounds and resize terminals with padding
270    ///
271    /// This should be called before creating splits to ensure panes are sized correctly.
272    /// The padding parameter accounts for content inset from pane edges.
273    pub fn set_pane_bounds_with_padding(
274        &mut self,
275        bounds: crate::pane::PaneBounds,
276        cell_width: f32,
277        cell_height: f32,
278        padding: f32,
279    ) {
280        if self.pane_manager.is_none() {
281            let mut pm = PaneManager::new();
282            pm.set_bounds(bounds);
283            self.pane_manager = Some(pm);
284        } else if let Some(ref mut pm) = self.pane_manager {
285            pm.set_bounds(bounds);
286            pm.resize_all_terminals_with_padding(cell_width, cell_height, padding, 0.0);
287        }
288    }
289
290    /// Focus the pane at the given pixel coordinates
291    ///
292    /// Returns the ID of the newly focused pane, or None if no pane at that position
293    pub fn focus_pane_at(&mut self, x: f32, y: f32) -> Option<crate::pane::PaneId> {
294        if let Some(ref mut pm) = self.pane_manager {
295            pm.focus_pane_at(x, y)
296        } else {
297            None
298        }
299    }
300
301    /// Get the ID of the currently focused pane
302    pub fn focused_pane_id(&self) -> Option<crate::pane::PaneId> {
303        self.pane_manager
304            .as_ref()
305            .and_then(|pm| pm.focused_pane_id())
306    }
307
308    /// Check if a specific pane is focused
309    pub fn is_pane_focused(&self, pane_id: crate::pane::PaneId) -> bool {
310        self.focused_pane_id() == Some(pane_id)
311    }
312
313    /// Navigate to an adjacent pane
314    pub fn navigate_pane(&mut self, direction: NavigationDirection) {
315        if let Some(ref mut pm) = self.pane_manager {
316            pm.navigate(direction);
317        }
318    }
319
320    /// Check if a position is on a divider
321    pub fn is_on_divider(&self, x: f32, y: f32) -> bool {
322        self.pane_manager
323            .as_ref()
324            .is_some_and(|pm| pm.is_on_divider(x, y))
325    }
326
327    /// Find divider at position
328    ///
329    /// Returns the divider index if found
330    pub fn find_divider_at(&self, x: f32, y: f32) -> Option<usize> {
331        self.pane_manager
332            .as_ref()
333            .and_then(|pm| pm.find_divider_at(x, y, pm.divider_hit_padding()))
334    }
335
336    /// Get divider info by index
337    pub fn get_divider(&self, index: usize) -> Option<crate::pane::DividerRect> {
338        self.pane_manager
339            .as_ref()
340            .and_then(|pm| pm.get_divider(index))
341    }
342
343    /// Drag a divider to a new position
344    pub fn drag_divider(&mut self, divider_index: usize, x: f32, y: f32) {
345        if let Some(ref mut pm) = self.pane_manager {
346            pm.drag_divider(divider_index, x, y);
347        }
348    }
349
350    /// Restore a pane layout from a saved session
351    ///
352    /// Replaces the current single-pane layout with a saved pane tree.
353    /// Each leaf in the tree gets a new terminal session with the saved CWD.
354    /// If the build fails, the tab keeps its existing single pane.
355    pub fn restore_pane_layout(
356        &mut self,
357        layout: &crate::session::SessionPaneNode,
358        config: &Config,
359        runtime: Arc<Runtime>,
360    ) {
361        let mut pm = PaneManager::new();
362        pm.set_divider_width(config.panes.pane_divider_width.unwrap_or(1.0));
363        pm.set_divider_hit_width(config.panes.pane_divider_hit_width);
364
365        match pm.build_from_layout(layout, config, runtime) {
366            Ok(()) => {
367                log::info!(
368                    "Restored pane layout for tab {} ({} panes)",
369                    self.id,
370                    pm.pane_count()
371                );
372                self.pane_manager = Some(pm);
373            }
374            Err(e) => {
375                log::warn!(
376                    "Failed to restore pane layout for tab {}: {}, keeping single pane",
377                    self.id,
378                    e
379                );
380            }
381        }
382    }
383
384    /// Start refresh tasks for all panes that don't already have one.
385    ///
386    /// Must be called after pane creation (split, restore) to ensure each pane's
387    /// terminal output triggers redraws. Without this, only `tab.terminal` is polled
388    /// by the tab refresh task, so secondary panes never trigger redraws.
389    pub fn start_pane_refresh_tasks(
390        &mut self,
391        runtime: Arc<Runtime>,
392        window: Arc<winit::window::Window>,
393        active_fps: u32,
394        inactive_fps: u32,
395    ) {
396        if let Some(ref mut pm) = self.pane_manager {
397            let is_tab_active = self.is_active.load(Ordering::Relaxed);
398            for pane in pm.all_panes_mut() {
399                pane.is_active.store(is_tab_active, Ordering::Relaxed);
400                if pane.refresh_task.is_none() {
401                    pane.start_refresh_task(
402                        Arc::clone(&runtime),
403                        Arc::clone(&window),
404                        active_fps,
405                        inactive_fps,
406                    );
407                }
408            }
409        }
410    }
411}