Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions kaku-gui/src/termwindow/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ enum EventState {
InProgressWithQueued(Option<PaneId>),
}

/// State tracked during a live split-divider drag.
struct SplitDragState {
tab_id: TabId,
}

pub struct TermWindow {
pub window: Option<Window>,
pub config: ConfigHandle,
Expand Down Expand Up @@ -440,6 +445,9 @@ pub struct TermWindow {

ui_items: Vec<UIItem>,
dragging: Option<(UIItem, MouseEvent)>,
/// Tracks state during a live split-drag so we can defer PTY
/// notification until the drag ends.
split_drag_state: Option<SplitDragState>,

modal: RefCell<Option<Rc<dyn Modal>>>,

Expand Down Expand Up @@ -783,6 +791,7 @@ impl TermWindow {
semantic_zones: HashMap::new(),
ui_items: vec![],
dragging: None,
split_drag_state: None,
last_ui_item: None,
is_click_to_focus_window: false,
key_table_state: KeyTableState::default(),
Expand Down
49 changes: 44 additions & 5 deletions kaku-gui/src/termwindow/mouseevent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,15 @@ impl super::TermWindow {
return;
}
if press == &MousePress::Left && self.dragging.take().is_some() {
// Completed a drag
// Completed a split drag: notify PTY of final sizes
// using the tab_id captured at drag start.
if let Some(state) = self.split_drag_state.take() {
let mux = Mux::get();
if let Some(tab) = mux.get_tab(state.tab_id) {
tab.flush_pane_pty_sizes();
context.invalidate();
}
}
return;
}
}
Expand Down Expand Up @@ -261,17 +269,48 @@ impl super::TermWindow {
context: &dyn WindowOps,
) {
let mux = Mux::get();
let tab = match mux.get_active_tab_for_window(self.mux_window_id) {
Some(tab) => tab,
None => return,

// On the first drag event, capture the tab_id from the active tab.
// All subsequent frames (and the final release) use this tab_id
// so we always operate on the same tab even if tabs switch mid-drag.
let tab = if let Some(ref state) = self.split_drag_state {
match mux.get_tab(state.tab_id) {
Some(tab) => tab,
None => {
// Tab was closed mid-drag; clear stale state and
// fall back to the current active tab.
self.split_drag_state = None;
let tab = match mux.get_active_tab_for_window(self.mux_window_id) {
Some(tab) => tab,
None => return,
};
self.split_drag_state = Some(super::SplitDragState {
tab_id: tab.tab_id(),
});
tab
}
}
} else {
let tab = match mux.get_active_tab_for_window(self.mux_window_id) {
Some(tab) => tab,
None => return,
};
self.split_drag_state = Some(super::SplitDragState {
tab_id: tab.tab_id(),
});
tab
};

let delta = match split.direction {
SplitDirection::Horizontal => (x as isize).saturating_sub(split.left as isize),
SplitDirection::Vertical => (y as isize).saturating_sub(split.top as isize),
};

if delta != 0 {
tab.resize_split_by(split.index, delta);
// Use visual-only resize during drag: updates terminal state
// for smooth content reflow but does NOT notify the PTY,
// so the shell won't receive rapid SIGWINCH signals.
tab.resize_split_by_visual(split.index, delta);
if let Some(split) = tab.iter_splits().into_iter().nth(split.index) {
item.item_type = UIItemType::Split(split);
context.invalidate();
Expand Down
5 changes: 5 additions & 0 deletions mux/src/localpane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,11 @@ impl Pane for LocalPane {
Ok(())
}

fn resize_visual(&self, size: TerminalSize) -> Result<(), Error> {
self.terminal.lock().resize(size);
Ok(())
}

fn writer(&self) -> MappedMutexGuard<'_, dyn std::io::Write> {
Mux::get().record_input_for_current_identity();
MutexGuard::map(self.writer.lock(), |writer| {
Expand Down
7 changes: 7 additions & 0 deletions mux/src/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,13 @@ pub trait Pane: Downcast + Send + Sync {
fn reader(&self) -> anyhow::Result<Option<Box<dyn std::io::Read + Send>>>;
fn writer(&self) -> MappedMutexGuard<'_, dyn std::io::Write>;
fn resize(&self, size: TerminalSize) -> anyhow::Result<()>;
/// Resize terminal state only without notifying the PTY.
/// Used during live split-drag to provide smooth visual feedback
/// without sending rapid SIGWINCH to the shell.
/// The default implementation falls back to `resize`.
fn resize_visual(&self, size: TerminalSize) -> anyhow::Result<()> {
self.resize(size)
}
/// Called as a hint that the pane is being resized as part of
/// a zoom-to-fill-all-the-tab-space operation.
fn set_zoomed(&self, _zoomed: bool) {}
Expand Down
92 changes: 92 additions & 0 deletions mux/src/tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,30 @@ impl Tab {
self.inner.lock().resize_split_by(split_index, delta)
}

/// Like `resize_split_by` but only updates terminal state without
/// notifying the PTY, so the shell does not receive SIGWINCH.
/// Used during live split-drag for smooth visual feedback.
pub fn resize_split_by_visual(&self, split_index: usize, delta: isize) {
self.inner.lock().resize_split_by_visual(split_index, delta)
}

/// Notify the PTY of the current size for every pane.
/// Called after a visual-only split drag completes so each shell
/// receives exactly one SIGWINCH with the final size.
pub fn flush_pane_pty_sizes(&self) {
for pos in self.iter_panes_ignoring_zoom() {
let dims = pos.pane.get_dimensions();
let size = TerminalSize {
rows: pos.height,
cols: pos.width,
pixel_width: pos.pixel_width,
pixel_height: pos.pixel_height,
dpi: dims.dpi,
};
let _ = pos.pane.resize(size);
}
}

/// Adjusts the size of the active pane in the specified direction
/// by the specified amount.
pub fn adjust_pane_size(&self, direction: PaneDirection, amount: usize) {
Expand Down Expand Up @@ -1291,6 +1315,35 @@ impl TabInner {
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabResized(self.id)));
}

fn resize_split_by_visual(&mut self, split_index: usize, delta: isize) {
if self.zoomed.is_some() {
return;
}

let mut cursor = self.pane.take().unwrap().cursor();
let mut index = 0;

loop {
if !cursor.is_leaf() {
if index == split_index {
break;
}
index += 1;
}
match cursor.preorder_next() {
Ok(c) => cursor = c,
Err(c) => {
self.pane.replace(c.tree());
return;
}
}
}

self.adjust_node_at_cursor(&mut cursor, delta);
self.cascade_size_from_cursor_visual(cursor);
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabResized(self.id)));
}

fn adjust_node_at_cursor(&mut self, cursor: &mut Cursor, delta: isize) {
let cell_dimensions = self.cell_dimensions();
if let Ok(Some(node)) = cursor.node_mut() {
Expand Down Expand Up @@ -1374,6 +1427,45 @@ impl TabInner {
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabResized(self.id)));
}

/// Like `cascade_size_from_cursor` but calls `resize_visual` instead of
/// `resize`, so only terminal state is updated without PTY notification.
fn cascade_size_from_cursor_visual(&mut self, mut cursor: Cursor) {
match cursor.preorder_next() {
Ok(c) => cursor = c,
Err(c) => {
self.pane.replace(c.tree());
return;
}
}
let root_size = self.size;

loop {
let pane_size = if let Some((branch, Some(parent))) = cursor.path_to_root().next() {
if branch == PathBranch::IsRight {
parent.second
} else {
parent.first
}
} else {
root_size
};

if cursor.is_leaf() {
cursor.leaf_mut().map(|pane| pane.resize_visual(pane_size));
} else {
self.apply_pane_size(pane_size, &mut cursor);
}
match cursor.preorder_next() {
Ok(c) => cursor = c,
Err(c) => {
self.pane.replace(c.tree());
break;
}
}
}
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabResized(self.id)));
}

fn adjust_pane_size(&mut self, direction: PaneDirection, amount: usize) {
if self.zoomed.is_some() {
return;
Expand Down
Loading