feat(tui): focus owned panes on click

This commit is contained in:
Felipe Coury
2026-07-04 21:00:37 -03:00
parent 589b9dde10
commit 3cb7cef03d
14 changed files with 484 additions and 35 deletions
+3
View File
@@ -1303,6 +1303,9 @@ See the Codex keymap documentation for supported actions and examples."
.await;
}
}
TuiEvent::MousePrimaryPress(event) => {
self.handle_owned_screen_mouse_primary_press(tui, event);
}
TuiEvent::Draw | TuiEvent::Resize => {
if self.backtrack_render_pending {
self.rebuild_transcript_after_backtrack(tui)?;
+32 -6
View File
@@ -102,9 +102,6 @@ impl App {
key_event: KeyEvent,
) {
if self.handle_conversation_pane_focus_key(key_event) {
if self.backtrack.primed {
self.reset_backtrack_state();
}
tui.frame_requester().schedule_frame();
return;
}
@@ -276,13 +273,26 @@ impl App {
};
}
fn handle_conversation_pane_focus_key(&mut self, key_event: KeyEvent) -> bool {
pub(super) fn focus_conversation_pane(&mut self, target: PaneSlot) -> bool {
if self.overlay.is_some()
|| self.chat_widget.by_slot(PaneSlot::Side).is_none()
|| !self.chat_widget.no_modal_or_popup_active()
|| self.chat_widget.by_slot(target).is_none()
{
return false;
}
if self.backtrack.primed {
self.reset_backtrack_state();
}
if self.chat_widget.focused_slot() == target {
return true;
}
self.chat_widget.focus(target)
}
fn handle_conversation_pane_focus_key(&mut self, key_event: KeyEvent) -> bool {
if self.chat_widget.by_slot(PaneSlot::Side).is_none() {
return false;
}
let composer_is_empty = self.chat_widget.composer_text_with_pending().is_empty();
let allow_word_motion_fallback = !self.enhanced_keys_supported && composer_is_empty;
@@ -299,7 +309,7 @@ impl App {
} else {
return false;
};
self.chat_widget.focus(target)
self.focus_conversation_pane(target)
}
pub(super) fn should_handle_backtrack_esc(&self, key_event: KeyEvent) -> bool {
@@ -395,6 +405,22 @@ mod tests {
);
}
#[tokio::test]
async fn pane_focus_key_clears_primed_backtrack_before_switching() {
let mut app = make_test_app().await;
install_side_pane(&mut app).await;
app.backtrack.primed = true;
app.chat_widget.show_esc_backtrack_hint();
assert!(app.handle_conversation_pane_focus_key(KeyEvent::new(
KeyCode::Char('2'),
KeyModifiers::ALT,
)));
assert_eq!(app.chat_widget.focused_slot(), PaneSlot::Side);
assert!(!app.backtrack.primed);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn pane_focus_does_not_steal_word_motion_from_nonempty_draft() {
+37 -2
View File
@@ -21,6 +21,7 @@ use ratatui::widgets::Widget;
use super::*;
use crate::AltScreenBehavior;
use crate::key_hint::is_plain_text_key_event;
use crate::tui::MousePrimaryPressEvent;
use crate::tui::MouseScrollEvent;
const MIN_SPLIT_PANE_WIDTH: u16 = 41;
@@ -31,6 +32,7 @@ const PANE_HEADER_HEIGHT: u16 = 1;
pub(super) struct OwnedScreen {
viewport: ConversationViewport,
replay_in_progress: bool,
last_pane_area: Rect,
last_conversation_area: Rect,
}
@@ -101,6 +103,7 @@ impl OwnedScreen {
keymap,
),
replay_in_progress: false,
last_pane_area: Rect::default(),
last_conversation_area: Rect::default(),
}
}
@@ -177,7 +180,8 @@ impl OwnedScreen {
true
}
fn clear_last_conversation_area(&mut self) {
fn clear_last_render_areas(&mut self) {
self.last_pane_area = Rect::default();
self.last_conversation_area = Rect::default();
}
}
@@ -251,6 +255,7 @@ fn render_pane(
let pane = panes.by_slot_mut(slot)?;
pane.chat_widget.update_owned_screen_width(body_area.width);
let screen = pane.owned_screen.as_mut()?;
screen.last_pane_area = area;
Some(screen.render(&pane.chat_widget, body_area, buffer))
}
@@ -266,7 +271,7 @@ fn render_layout(
.by_slot_mut(slot)
.and_then(|pane| pane.owned_screen.as_mut())
{
screen.clear_last_conversation_area();
screen.clear_last_render_areas();
}
}
@@ -392,6 +397,36 @@ impl App {
false
}
pub(super) fn handle_owned_screen_mouse_primary_press(
&mut self,
tui: &mut tui::Tui,
event: MousePrimaryPressEvent,
) -> bool {
if !self.chat_widget.has_side() {
return false;
}
let position = Position::new(event.column, event.row);
let target = [PaneSlot::Parent, PaneSlot::Side].into_iter().find(|slot| {
self.chat_widget
.by_slot(*slot)
.and_then(|pane| pane.owned_screen.as_ref())
.is_some_and(|screen| screen.last_pane_area.contains(position))
});
let Some(target) = target else {
return false;
};
let focus_changed = self.chat_widget.focused_slot() != target;
let backtrack_was_primed = self.backtrack.primed;
if !self.focus_conversation_pane(target) {
return false;
}
if focus_changed || backtrack_was_primed {
tui.frame_requester().schedule_frame();
}
true
}
pub(crate) fn sync_owned_screen_cells(&mut self) {
let cells = self.chat_widget.transcript_cells.clone();
if let Some(screen) = &mut self.chat_widget.owned_screen {
+318 -2
View File
@@ -17,6 +17,7 @@ use crate::chatwidget::tests::constructor::make_chatwidget_for_pane;
use crate::chatwidget::tests::constructor::make_chatwidget_for_pane_with_sender;
use crate::chatwidget::tests::make_chatwidget_manual_with_sender;
use crate::file_search::FileSearchManager;
use crate::tui::MousePrimaryPressEvent;
use crate::tui::MouseScrollDirection;
use crate::tui::MouseScrollEvent;
use codex_app_server_protocol::ConfigWarningNotification;
@@ -362,6 +363,60 @@ async fn renders_closed_parent_read_only_while_side_remains_focused() {
);
}
#[tokio::test]
async fn primary_press_focuses_closed_parent_without_enabling_input() {
let mut app = app_with_owned_side().await;
seed_pane(&mut app, PaneSlot::Parent, "", &["parent transcript"]);
seed_pane(&mut app, PaneSlot::Side, "side draft", &["side transcript"]);
app.chat_widget
.by_slot_mut(PaneSlot::Parent)
.expect("parent pane")
.mark_thread_closed();
assert!(app.chat_widget.focus(PaneSlot::Side));
let _terminal = render_app(&mut app, /*width*/ 83, /*height*/ 12);
let parent_area = app
.chat_widget
.by_slot(PaneSlot::Parent)
.and_then(|pane| pane.owned_screen.as_ref())
.expect("parent screen")
.last_pane_area;
let mut tui = crate::tui::test_support::make_test_tui().expect("create test TUI");
assert!(app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: parent_area.x,
row: parent_area.y,
},
));
assert_eq!(app.chat_widget.focused_slot(), PaneSlot::Parent);
let mut rendered_cursor = None;
let mut terminal =
Terminal::new(TestBackend::new(/*width*/ 83, /*height*/ 12)).expect("create terminal");
terminal
.draw(|frame| {
rendered_cursor = Some(
render_layout(
&mut app.chat_widget,
OwnedScreenLayout::new(frame.area(), /*has_side*/ true, PaneSlot::Parent),
PaneSlot::Parent,
frame.buffer_mut(),
)
.expect("render closed parent")
.cursor,
);
})
.expect("render owned panes");
assert_eq!(rendered_cursor, Some(None));
assert_eq!(
app.chat_widget
.by_slot(PaneSlot::Side)
.expect("side pane")
.composer_text_with_pending(),
"side draft"
);
}
#[tokio::test]
async fn narrow_layout_renders_only_the_focused_side() {
let mut app = app_with_owned_side().await;
@@ -372,6 +427,7 @@ async fn narrow_layout_renders_only_the_focused_side() {
&["PARENT MUST BE HIDDEN"],
);
seed_pane(&mut app, PaneSlot::Side, "side draft", &["side transcript"]);
let _wide = render_app(&mut app, /*width*/ 83, /*height*/ 14);
assert!(app.chat_widget.focus(PaneSlot::Side));
let terminal = render_app(&mut app, /*width*/ 82, /*height*/ 14);
@@ -381,9 +437,16 @@ async fn narrow_layout_renders_only_the_focused_side() {
app.chat_widget
.by_slot(PaneSlot::Parent)
.and_then(|pane| pane.owned_screen.as_ref())
.map(|screen| screen.last_conversation_area),
Some(Rect::default())
.map(|screen| (screen.last_pane_area, screen.last_conversation_area)),
Some((Rect::default(), Rect::default()))
);
let mut tui = crate::tui::test_support::make_test_tui().expect("create test TUI");
assert!(app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent { column: 2, row: 2 },
));
assert_eq!(app.chat_widget.focused_slot(), PaneSlot::Side);
}
#[tokio::test]
@@ -402,6 +465,259 @@ async fn terminal_cursor_tracks_only_the_focused_pane() {
assert!(side_cursor.x > 41);
}
#[tokio::test]
async fn primary_press_focuses_visible_pane_regions() {
let mut app = app_with_owned_side().await;
seed_pane(
&mut app,
PaneSlot::Parent,
"parent draft",
&["parent transcript"],
);
seed_pane(&mut app, PaneSlot::Side, "side draft", &["side transcript"]);
let _terminal = render_app(&mut app, /*width*/ 83, /*height*/ 16);
let (parent_area, parent_conversation_area, side_area, side_conversation_area) = {
let parent = app
.chat_widget
.by_slot(PaneSlot::Parent)
.and_then(|pane| pane.owned_screen.as_ref())
.expect("parent screen");
let side = app
.chat_widget
.by_slot(PaneSlot::Side)
.and_then(|pane| pane.owned_screen.as_ref())
.expect("side screen");
(
parent.last_pane_area,
parent.last_conversation_area,
side.last_pane_area,
side.last_conversation_area,
)
};
let region_pairs = [
((parent_area.x, parent_area.y), (side_area.x, side_area.y)),
(
(parent_conversation_area.x, parent_conversation_area.y),
(side_conversation_area.x, side_conversation_area.y),
),
(
(parent_area.x, parent_conversation_area.bottom()),
(side_area.x, side_conversation_area.bottom()),
),
(
(parent_area.x, parent_area.bottom() - 1),
(side_area.x, side_area.bottom() - 1),
),
];
let mut tui = crate::tui::test_support::make_test_tui().expect("create test TUI");
for (parent_position, side_position) in region_pairs {
app.backtrack.primed = true;
assert!(app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: side_position.0,
row: side_position.1,
},
));
assert_eq!(app.chat_widget.focused_slot(), PaneSlot::Side);
assert!(!app.backtrack.primed);
assert!(app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: parent_position.0,
row: parent_position.1,
},
));
assert_eq!(app.chat_widget.focused_slot(), PaneSlot::Parent);
}
app.backtrack.primed = true;
assert!(app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: parent_area.x,
row: parent_area.y,
},
));
assert!(!app.backtrack.primed);
assert!(!app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: parent_area.right(),
row: parent_area.y,
},
));
assert!(!app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: side_area.right(),
row: side_area.y,
},
));
assert_eq!(app.chat_widget.focused_slot(), PaneSlot::Parent);
}
#[tokio::test]
async fn primary_press_preserves_pane_drafts_and_cursors() {
let mut app = app_with_owned_side().await;
seed_pane(
&mut app,
PaneSlot::Parent,
"parent draft",
&["parent transcript"],
);
seed_pane(&mut app, PaneSlot::Side, "side draft", &["side transcript"]);
for (slot, cursor_moves) in [(PaneSlot::Parent, 1), (PaneSlot::Side, 2)] {
for _ in 0..cursor_moves {
app.chat_widget
.by_slot_mut(slot)
.expect("installed pane")
.handle_key_event(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
}
}
let mut parent_terminal = render_app(&mut app, /*width*/ 83, /*height*/ 16);
let parent_cursor = parent_terminal
.get_cursor_position()
.expect("parent cursor");
let (parent_area, parent_conversation_area, side_area, side_conversation_area) = {
let parent = app
.chat_widget
.by_slot(PaneSlot::Parent)
.and_then(|pane| pane.owned_screen.as_ref())
.expect("parent screen");
let side = app
.chat_widget
.by_slot(PaneSlot::Side)
.and_then(|pane| pane.owned_screen.as_ref())
.expect("side screen");
(
parent.last_pane_area,
parent.last_conversation_area,
side.last_pane_area,
side.last_conversation_area,
)
};
assert!(app.chat_widget.focus(PaneSlot::Side));
let mut side_terminal_before_click =
render_app(&mut app, /*width*/ 83, /*height*/ 16);
let side_cursor_before_click = side_terminal_before_click
.get_cursor_position()
.expect("side cursor before click");
assert!(app.chat_widget.focus(PaneSlot::Parent));
let _parent_terminal = render_app(&mut app, /*width*/ 83, /*height*/ 16);
let mut tui = crate::tui::test_support::make_test_tui().expect("create test TUI");
assert!(app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: side_area.x,
row: side_conversation_area.bottom(),
},
));
let mut side_terminal = render_app(&mut app, /*width*/ 83, /*height*/ 16);
let side_cursor = side_terminal.get_cursor_position().expect("side cursor");
assert_eq!(side_cursor, side_cursor_before_click);
assert_snapshot!(
"owned_screen_wide_split_side_focused_by_click",
side_terminal.backend()
);
let buffer = side_terminal.backend().buffer();
assert!(
buffer[(parent_area.x + 1, parent_area.y)]
.style()
.add_modifier
.contains(ratatui::style::Modifier::DIM)
);
assert!(
buffer[(side_area.x + 1, side_area.y)]
.style()
.add_modifier
.contains(ratatui::style::Modifier::BOLD)
);
assert_eq!(
(
app.chat_widget
.by_slot(PaneSlot::Parent)
.expect("parent pane")
.composer_text_with_pending(),
app.chat_widget
.by_slot(PaneSlot::Side)
.expect("side pane")
.composer_text_with_pending(),
),
("parent draft".to_string(), "side draft".to_string())
);
assert!(app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: parent_area.x,
row: parent_conversation_area.bottom(),
},
));
let mut parent_terminal = render_app(&mut app, /*width*/ 83, /*height*/ 16);
assert_eq!(
parent_terminal
.get_cursor_position()
.expect("restored parent cursor"),
parent_cursor
);
assert!(app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: side_area.x,
row: side_conversation_area.bottom(),
},
));
let mut side_terminal = render_app(&mut app, /*width*/ 83, /*height*/ 16);
assert_eq!(
side_terminal
.get_cursor_position()
.expect("restored side cursor"),
side_cursor
);
}
#[tokio::test]
async fn primary_press_does_not_switch_behind_overlay_or_popup() {
let mut app = app_with_owned_side().await;
let _terminal = render_app(&mut app, /*width*/ 83, /*height*/ 12);
let side_area = app
.chat_widget
.by_slot(PaneSlot::Side)
.and_then(|pane| pane.owned_screen.as_ref())
.expect("side screen")
.last_pane_area;
let mut tui = crate::tui::test_support::make_test_tui().expect("create test TUI");
app.overlay = Some(Overlay::new_transcript(
Vec::new(),
app.keymap.pager.clone(),
));
assert!(!app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: side_area.x,
row: side_area.y,
},
));
assert_eq!(app.chat_widget.focused_slot(), PaneSlot::Parent);
app.overlay = None;
let keymap = app.keymap.clone();
app.chat_widget.open_keymap_debug(&keymap);
assert!(!app.handle_owned_screen_mouse_primary_press(
&mut tui,
MousePrimaryPressEvent {
column: side_area.x,
row: side_area.y,
},
));
assert_eq!(app.chat_widget.focused_slot(), PaneSlot::Parent);
}
#[tokio::test]
async fn mouse_wheel_routes_by_pointer_without_changing_focus() {
let mut app = app_with_owned_side().await;
@@ -0,0 +1,20 @@
---
source: tui/src/app/owned_screen_tests.rs
expression: side_terminal.backend()
---
" Parent │ Side ⌥ + 1 / ⌥ + 2 focus "
"parent transcript │side transcript "
" │ "
" │╭───────────────────────────────────────╮"
" ││ >_ OpenAI Codex (v0.0.0) │"
" ││ │"
" ││ model: loading /model to change │"
" ││ directory: /tmp/project │"
" │╰───────────────────────────────────────╯"
" │ "
" │ "
" │ "
" │ "
"› parent draft │› side draft "
" │ "
" gpt-5.5 default · /tmp/project │ gpt-5.5 default · /tmp/project "
+1 -1
View File
@@ -96,7 +96,7 @@ pub(crate) async fn run_cwd_selection_prompt(
if let Some(event) = events.next().await {
match event {
TuiEvent::Key(key_event) => screen.handle_key(key_event),
TuiEvent::Paste(_) | TuiEvent::MouseScroll(_) => {}
TuiEvent::Paste(_) | TuiEvent::MouseScroll(_) | TuiEvent::MousePrimaryPress(_) => {}
TuiEvent::Draw | TuiEvent::Resize => {
tui.draw(u16::MAX, |frame| {
frame.render_widget_ref(&screen, frame.area());
@@ -99,7 +99,7 @@ pub(crate) async fn run_external_agent_config_migration_prompt(
if let Some(event) = events.next().await {
match event {
TuiEvent::Key(key_event) => screen.handle_key(key_event),
TuiEvent::Paste(_) | TuiEvent::MouseScroll(_) => {}
TuiEvent::Paste(_) | TuiEvent::MouseScroll(_) | TuiEvent::MousePrimaryPress(_) => {}
TuiEvent::Draw | TuiEvent::Resize => {
let _ = tui.draw(u16::MAX, |frame| {
frame.render_widget_ref(&screen, frame.area());
+1 -1
View File
@@ -152,7 +152,7 @@ pub(crate) async fn run_model_migration_prompt(
if let Some(event) = events.next().await {
match event {
TuiEvent::Key(key_event) => screen.handle_key(key_event),
TuiEvent::Paste(_) | TuiEvent::MouseScroll(_) => {}
TuiEvent::Paste(_) | TuiEvent::MouseScroll(_) | TuiEvent::MousePrimaryPress(_) => {}
TuiEvent::Draw | TuiEvent::Resize => {
let _ = alt.tui.draw(u16::MAX, |frame| {
frame.render_widget_ref(&screen, frame.area());
@@ -509,7 +509,7 @@ pub(crate) async fn run_onboarding_app(
TuiEvent::Paste(text) => {
onboarding_screen.handle_paste(text);
}
TuiEvent::MouseScroll(_) => {}
TuiEvent::MouseScroll(_) | TuiEvent::MousePrimaryPress(_) => {}
TuiEvent::Draw | TuiEvent::Resize => {
if !did_full_clear_after_success
&& onboarding_screen.steps.iter().any(|step| {
+1 -1
View File
@@ -475,7 +475,7 @@ async fn run_session_picker_with_loader(
TuiEvent::Paste(pasted) => {
state.handle_paste(pasted);
}
TuiEvent::MouseScroll(_) => {}
TuiEvent::MouseScroll(_) | TuiEvent::MousePrimaryPress(_) => {}
TuiEvent::Draw | TuiEvent::Resize => {
if let Ok(size) = alt.tui.terminal.size() {
let list_height =
+1 -1
View File
@@ -166,7 +166,7 @@ async fn run_startup_hooks_review_app(
}
}
}
TuiEvent::Paste(_) | TuiEvent::MouseScroll(_) => {}
TuiEvent::Paste(_) | TuiEvent::MouseScroll(_) | TuiEvent::MousePrimaryPress(_) => {}
TuiEvent::Draw | TuiEvent::Resize => draw_view(tui, &view)?,
}
}
+8
View File
@@ -519,6 +519,8 @@ pub enum TuiEvent {
Paste(String),
/// A vertical mouse-wheel event reported while an owned screen has mouse capture.
MouseScroll(MouseScrollEvent),
/// A primary mouse-button press with terminal coordinates.
MousePrimaryPress(MousePrimaryPressEvent),
/// A terminal size notification that should be handled as resize-sensitive draw work.
///
/// Resize is separate from `Draw` so the app can run feature-gated pre-render logic without
@@ -535,6 +537,12 @@ pub struct MouseScrollEvent {
pub row: u16,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MousePrimaryPressEvent {
pub column: u16,
pub row: u16,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MouseScrollDirection {
Up,
+59 -18
View File
@@ -34,6 +34,7 @@ use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::wrappers::WatchStream;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
use super::MousePrimaryPressEvent;
use super::MouseScrollDirection;
use super::MouseScrollEvent;
use super::TuiEvent;
@@ -179,11 +180,11 @@ impl<S: EventSource + Default + Unpin> TuiEventStream<S> {
/// Poll the shared crossterm stream for the next mapped `TuiEvent`.
///
/// This skips events we don't use (mouse clicks, focus loss, etc.) and keeps polling until it yields
/// This skips events we don't use (mouse movement, focus loss, etc.) and keeps polling until it yields
/// a mapped event, hits `Pending`, or sees EOF/error. When the broker is paused, it drops
/// the underlying stream and returns `Pending` to fully release stdin.
pub fn poll_crossterm_event(&mut self, cx: &mut Context<'_>) -> Poll<Option<TuiEvent>> {
// Some crossterm events map to None (e.g. FocusLost, mouse clicks); loop so we keep polling
// Some crossterm events map to None (e.g. FocusLost, mouse movement); loop so we keep polling
// until we return a mapped event, hit Pending, or see EOF/error.
for _ in 0..MAX_SKIPPED_EVENTS_PER_POLL {
let poll_result = {
@@ -264,23 +265,30 @@ impl<S: EventSource + Default + Unpin> TuiEventStream<S> {
}
Event::Resize(_, _) => Some(TuiEvent::Resize),
Event::Paste(pasted) => Some(TuiEvent::Paste(pasted)),
Event::Mouse(mouse_event) => {
let direction = match mouse_event.kind {
MouseEventKind::ScrollUp => MouseScrollDirection::Up,
MouseEventKind::ScrollDown => MouseScrollDirection::Down,
MouseEventKind::Down(_)
| MouseEventKind::Up(_)
| MouseEventKind::Drag(_)
| MouseEventKind::Moved
| MouseEventKind::ScrollLeft
| MouseEventKind::ScrollRight => return None,
};
Some(TuiEvent::MouseScroll(MouseScrollEvent {
direction,
Event::Mouse(mouse_event) => match mouse_event.kind {
MouseEventKind::ScrollUp => Some(TuiEvent::MouseScroll(MouseScrollEvent {
direction: MouseScrollDirection::Up,
column: mouse_event.column,
row: mouse_event.row,
}))
}
})),
MouseEventKind::ScrollDown => Some(TuiEvent::MouseScroll(MouseScrollEvent {
direction: MouseScrollDirection::Down,
column: mouse_event.column,
row: mouse_event.row,
})),
MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
Some(TuiEvent::MousePrimaryPress(MousePrimaryPressEvent {
column: mouse_event.column,
row: mouse_event.row,
}))
}
MouseEventKind::Down(_)
| MouseEventKind::Up(_)
| MouseEventKind::Drag(_)
| MouseEventKind::Moved
| MouseEventKind::ScrollLeft
| MouseEventKind::ScrollRight => None,
},
Event::FocusGained => {
self.terminal_focused.store(true, Ordering::Relaxed);
crate::terminal_palette::requery_default_colors();
@@ -498,6 +506,27 @@ mod tests {
assert!(matches!(next, Some(TuiEvent::Resize)));
}
#[tokio::test(flavor = "current_thread")]
async fn primary_mouse_press_maps_to_event() {
let (broker, handle, _draw_tx, draw_rx, terminal_focused) = setup();
let mut stream = make_stream(broker, draw_rx, terminal_focused);
handle.send(Ok(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 3,
row: 4,
modifiers: KeyModifiers::NONE,
})));
assert!(matches!(
stream.next().await,
Some(TuiEvent::MousePrimaryPress(MousePrimaryPressEvent {
column: 3,
row: 4,
}))
));
}
#[tokio::test(flavor = "current_thread")]
async fn vertical_mouse_wheel_maps_to_scroll_and_ignored_events_yield_to_draws() {
let (broker, handle, draw_tx, draw_rx, terminal_focused) = setup();
@@ -505,11 +534,23 @@ mod tests {
stream.poll_draw_first = false;
handle.send(Ok(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
kind: MouseEventKind::Down(MouseButton::Right),
column: 3,
row: 4,
modifiers: KeyModifiers::NONE,
})));
handle.send(Ok(Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: 3,
row: 4,
modifiers: KeyModifiers::NONE,
})));
handle.send(Ok(Event::Mouse(MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 4,
row: 4,
modifiers: KeyModifiers::NONE,
})));
for column in 0..=MAX_SKIPPED_EVENTS_PER_POLL {
handle.send(Ok(Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
+1 -1
View File
@@ -58,7 +58,7 @@ pub(crate) async fn run_update_prompt_if_needed(
if let Some(event) = events.next().await {
match event {
TuiEvent::Key(key_event) => screen.handle_key(key_event),
TuiEvent::Paste(_) | TuiEvent::MouseScroll(_) => {}
TuiEvent::Paste(_) | TuiEvent::MouseScroll(_) | TuiEvent::MousePrimaryPress(_) => {}
TuiEvent::Draw | TuiEvent::Resize => {
tui.draw(u16::MAX, |frame| {
frame.render_widget_ref(&screen, frame.area());