From 74947a086b96fe220b56e8ed82d6725f7eea9e42 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 3 Sep 2026 07:26:11 -0700 Subject: [PATCH] fix(cli): align playbook click cursor when slid --- crates/cli/src/app.rs | 40 ++++++++++++++++++++++++++++++++++-- crates/cli/src/app/editor.rs | 21 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/app.rs b/crates/cli/src/app.rs index ad12548b..6dbe9458 100644 --- a/crates/cli/src/app.rs +++ b/crates/cli/src/app.rs @@ -16700,6 +16700,7 @@ fn playbook_cursor_at_modal_point( scroll_offset: usize, col: u16, row: u16, + body_width: usize, ) -> Option { let inner_x = modal .x @@ -16718,10 +16719,9 @@ fn playbook_cursor_at_modal_point( // offset instead of being treated as a whole logical line. let target_row = (row.saturating_sub(inner_y) as usize).saturating_add(scroll_offset); let target_col = col.saturating_sub(inner_x) as usize; - let width = ui::playbook_modal_inner_width(modal); Some(playbook_normalize_playbook_cursor( buffer, - ui::playbook_visual_to_cursor(app, buffer, target_row, target_col, width), + ui::playbook_visual_to_cursor(app, buffer, target_row, target_col, body_width), )) } @@ -33123,6 +33123,42 @@ mod tests { server.abort(); } + #[tokio::test] + async fn playbook_click_on_slid_popup_uses_unclipped_wrap_width() { + use crossterm::event::{MouseButton, MouseEvent, MouseEventKind}; + + let (mut app, _dir, server) = empty_app().await; + app.playbook_popup = Some(playbook_popup_for_test( + "s1", + "abcdefghijklmnopQRST", + 0, + )); + + // A terminal-focused Playbook keeps its 20-column logical popup width + // while sliding four columns right, then clips at the pane edge. The + // body therefore paints at width 16 even though the clickable modal + // rectangle is only 16 columns wide (which would imply body width 12 + // if used for layout). The first cell of the painted continuation row + // is source offset 16, not 12. + app.layout.modal_area = Some(Rect::new(4, 0, 16, 20)); + app.layout.playbook_inner_area = Some(Rect::new(6, 2, 16, 16)); + + app.handle_playbook_mouse(&MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: 6, + row: 3, + modifiers: KeyModifiers::NONE, + }) + .await; + + assert_eq!( + app.playbook_popup.as_ref().unwrap().cursor, + 16, + "click mapping must use the full rendered body width, not the clipped hit rect" + ); + server.abort(); + } + #[tokio::test] async fn playbook_click_maps_through_scroll_offset_on_wrapped_row() { let (mut app, _dir, server) = empty_app().await; diff --git a/crates/cli/src/app/editor.rs b/crates/cli/src/app/editor.rs index c35a9775..cb4e1b68 100644 --- a/crates/cli/src/app/editor.rs +++ b/crates/cli/src/app/editor.rs @@ -1,12 +1,28 @@ use super::*; impl App { + /// Width the playbook body was laid out at in the last frame. + /// + /// A terminal-focused playbook slides right while retaining its full width, + /// but `modal_area` is clipped to the owning pane for hit-testing. Deriving + /// wrap width from that clipped rectangle makes click-to-caret resolve + /// against narrower lines than the ones on screen. The rendered inner area + /// keeps the logical width, so prefer it whenever it is available. + fn playbook_click_body_width(&self, modal: ratatui::layout::Rect) -> usize { + self.layout + .playbook_inner_area + .filter(|inner| inner.width > 0) + .map(|inner| inner.width as usize) + .unwrap_or_else(|| crate::ui::playbook_modal_inner_width(modal)) + } + pub(super) fn place_playbook_cursor( &mut self, modal: ratatui::layout::Rect, col: u16, row: u16, ) { + let body_width = self.playbook_click_body_width(modal); let cursor = { let app: &App = self; let Some(popup) = app.playbook_popup.as_ref() else { @@ -19,6 +35,7 @@ impl App { popup.scroll_offset, col, row, + body_width, ) .unwrap_or(0) }; @@ -514,6 +531,7 @@ impl App { } match ev.kind { MouseEventKind::Down(MouseButton::Left) => { + let body_width = self.playbook_click_body_width(modal); let cursor = { let app: &App = self; let Some(popup) = app.playbook_popup.as_ref() else { @@ -526,6 +544,7 @@ impl App { popup.scroll_offset, ev.column, modal_row, + body_width, ) .unwrap_or(0) }; @@ -567,6 +586,7 @@ impl App { true } MouseEventKind::Drag(MouseButton::Left) => { + let body_width = self.playbook_click_body_width(modal); let cursor = { let app: &App = self; let Some(popup) = app.playbook_popup.as_ref() else { @@ -579,6 +599,7 @@ impl App { popup.scroll_offset, ev.column, modal_row, + body_width, ) .unwrap_or(0) };