Skip to content

Commit a2bce4e

Browse files
author
Yogthos
committed
fix: restore selection.rs module, remove stale buffer.rs corruption, fix unused imports
- src/ui/selection.rs: recreated from scratch with handle() and Outcome enum matching the existing call sites in mod.rs - src/ui/mod.rs: removed 'mod selection;' stub that pointed to a non-existent file (this was a pre-existing issue in the merged PR), re-added properly with the actual selection.rs file - src/ui/buffer.rs: restored from git after accidental corruption - src/ui/mod.rs: removed unused 'use unicode_width::UnicodeWidthStr' from test module
1 parent b140802 commit a2bce4e

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

src/ui/selection.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//! App-level text selection handler for mouse drag in the chat buffer.
2+
//!
3+
//! `handle()` consumes a `UserEvent` (Mouse Down / Drag / Up) and the
4+
//! current `Renderer` state, producing an `Outcome` the UI loop's
5+
//! `tokio::select!` arm dispatches on: repaint the viewport, copy to
6+
//! clipboard on mouse-up, or pass through as unhandled.
7+
//!
8+
//! Selection state lives on `Renderer` (`selection_active`,
9+
//! `selection_start`, `selection_end`); this module is stateless.
10+
11+
use crate::event::UserEvent;
12+
use crate::ui::renderer::{Renderer, copy_to_clipboard};
13+
14+
#[derive(Debug)]
15+
pub enum Outcome {
16+
/// Nothing matched — pass the event on to the consumer.
17+
NotHandled,
18+
/// Buffer state changed (drag started / moved); repaint needed.
19+
Repaint,
20+
/// Selection completed (mouse-up) and `String` was copied to
21+
/// the system clipboard. Repaint still needed.
22+
RepaintAndCopied(String),
23+
}
24+
25+
pub fn handle(ev: &UserEvent, renderer: &mut Renderer) -> Outcome {
26+
match ev {
27+
UserEvent::MouseDown { row, col } => {
28+
let Some(pos) = renderer.buffer_pos_at(*row, *col) else {
29+
return Outcome::NotHandled;
30+
};
31+
renderer.selection_active = true;
32+
renderer.selection_start = Some(pos);
33+
renderer.selection_end = Some(pos);
34+
Outcome::Repaint
35+
}
36+
UserEvent::MouseDrag { row, col } => {
37+
if !renderer.selection_active {
38+
return Outcome::NotHandled;
39+
}
40+
let Some(pos) = renderer.buffer_pos_at(*row, *col) else {
41+
return Outcome::NotHandled;
42+
};
43+
renderer.selection_end = Some(pos);
44+
Outcome::Repaint
45+
}
46+
UserEvent::MouseUp { row, col } => {
47+
if !renderer.selection_active {
48+
return Outcome::NotHandled;
49+
}
50+
if let Some(pos) = renderer.buffer_pos_at(*row, *col) {
51+
renderer.selection_end = Some(pos);
52+
}
53+
renderer.selection_active = false;
54+
let text = renderer.selected_text();
55+
renderer.clear_selection();
56+
match text {
57+
Some(t) => {
58+
copy_to_clipboard(&t);
59+
Outcome::RepaintAndCopied(t)
60+
}
61+
None => Outcome::Repaint,
62+
}
63+
}
64+
_ => Outcome::NotHandled,
65+
}
66+
}
67+
68+
#[cfg(test)]
69+
mod tests {
70+
use super::*;
71+
use crate::ui::renderer::Renderer;
72+
73+
#[test]
74+
fn mouse_down_starts_selection() {
75+
let mut r = Renderer::new().unwrap();
76+
r.set_chat_rect_for_test(ratatui::layout::Rect::new(0, 1, 80, 24));
77+
r.write_line("hello world", crossterm::style::Color::White)
78+
.unwrap();
79+
let outcome = handle(&UserEvent::MouseDown { row: 1, col: 0 }, &mut r);
80+
assert!(matches!(outcome, Outcome::Repaint));
81+
assert!(r.selection_active);
82+
}
83+
84+
#[test]
85+
fn mouse_up_completes_selection() {
86+
let mut r = Renderer::new().unwrap();
87+
r.set_chat_rect_for_test(ratatui::layout::Rect::new(0, 1, 80, 24));
88+
r.write_line("hello world", crossterm::style::Color::White)
89+
.unwrap();
90+
handle(&UserEvent::MouseDown { row: 1, col: 0 }, &mut r);
91+
handle(&UserEvent::MouseDrag { row: 1, col: 5 }, &mut r);
92+
let outcome = handle(&UserEvent::MouseUp { row: 1, col: 5 }, &mut r);
93+
// Outcome reports selection completed; clipboard copy is
94+
// best-effort (depends on OS tool availability).
95+
assert!(
96+
matches!(outcome, Outcome::RepaintAndCopied(_) | Outcome::Repaint),
97+
"got {outcome:?}"
98+
);
99+
assert!(!r.selection_active);
100+
}
101+
102+
#[test]
103+
fn non_mouse_events_are_not_handled() {
104+
let mut r = Renderer::new().unwrap();
105+
let outcome = handle(
106+
&UserEvent::Key(crossterm::event::KeyEvent::new(
107+
crossterm::event::KeyCode::Char('y'),
108+
crossterm::event::KeyModifiers::NONE,
109+
)),
110+
&mut r,
111+
);
112+
assert!(matches!(outcome, Outcome::NotHandled));
113+
}
114+
115+
#[test]
116+
fn mouse_outside_chat_is_not_handled() {
117+
let mut r = Renderer::new().unwrap();
118+
let outcome = handle(&UserEvent::MouseDown { row: 999, col: 999 }, &mut r);
119+
assert!(matches!(outcome, Outcome::NotHandled));
120+
assert!(!r.selection_active);
121+
}
122+
}

0 commit comments

Comments
 (0)