1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//! Scroll and Panel Navigation for Chat View
//!
//! Contains panel focus management, scroll control, and smooth scrolling.
use super::{ChatPanel, ChatView, PanelScrollState};
// ═══════════════════════════════════════════════════════════════════════════════
// Panel Navigation
// ═══════════════════════════════════════════════════════════════════════════════
impl ChatView {
/// Focus next panel (Tab key)
pub fn focus_next_panel(&mut self) {
self.focused_panel = self.focused_panel.next();
}
/// Focus previous panel (Shift+Tab)
pub fn focus_prev_panel(&mut self) {
self.focused_panel = self.focused_panel.prev();
}
/// Focus a specific panel (for mouse clicks)
pub fn focus_panel(&mut self, panel: ChatPanel) {
self.focused_panel = panel;
}
/// Get the scroll state for the currently focused panel (mutable)
pub fn focused_scroll_mut(&mut self) -> Option<&mut PanelScrollState> {
match self.focused_panel {
ChatPanel::Conversation => Some(&mut self.conversation_scroll),
ChatPanel::Activity => Some(&mut self.activity_scroll),
ChatPanel::Input => None, // Input panel doesn't scroll
}
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// Scroll Control
// ═══════════════════════════════════════════════════════════════════════════════
impl ChatView {
/// Scroll down by one item
pub fn scroll_down(&mut self) {
// Don't update total here - add_message() and render() handle it
// This lets tests configure scroll state manually
match self.focused_panel {
ChatPanel::Input | ChatPanel::Conversation => {
self.conversation_scroll.scroll_down();
// Check if we reached the bottom
self.user_at_bottom = self.is_at_bottom();
}
ChatPanel::Activity => {
self.activity_scroll.scroll_down();
}
}
}
/// Scroll up by one item
pub fn scroll_up(&mut self) {
// Don't update total here - add_message() and render() handle it
match self.focused_panel {
ChatPanel::Input | ChatPanel::Conversation => {
self.conversation_scroll.scroll_up();
// User scrolled up = stop auto-following
self.user_at_bottom = false;
}
ChatPanel::Activity => {
self.activity_scroll.scroll_up();
}
}
}
/// Scroll to top
pub fn scroll_to_top(&mut self) {
match self.focused_panel {
ChatPanel::Input | ChatPanel::Conversation => {
self.conversation_scroll.scroll_to_top();
// Went to top = stop auto-following
self.user_at_bottom = false;
}
ChatPanel::Activity => {
self.activity_scroll.scroll_to_top();
}
}
}
/// Scroll to bottom
pub fn scroll_to_bottom(&mut self) {
match self.focused_panel {
ChatPanel::Input | ChatPanel::Conversation => {
self.conversation_scroll.scroll_to_bottom();
// Went to bottom = resume auto-following
self.user_at_bottom = true;
}
ChatPanel::Activity => {
self.activity_scroll.scroll_to_bottom();
}
}
}
/// Page down
pub fn page_down(&mut self) {
match self.focused_panel {
ChatPanel::Input | ChatPanel::Conversation => {
self.conversation_scroll.page_down();
// Check if we reached the bottom
self.user_at_bottom = self.is_at_bottom();
}
ChatPanel::Activity => {
self.activity_scroll.page_down();
}
}
}
/// Page up
pub fn page_up(&mut self) {
match self.focused_panel {
ChatPanel::Input | ChatPanel::Conversation => {
self.conversation_scroll.page_up();
// User scrolled up = stop auto-following
self.user_at_bottom = false;
}
ChatPanel::Activity => {
self.activity_scroll.page_up();
}
}
}
/// Check if conversation is scrolled to the bottom
pub(super) fn is_at_bottom(&self) -> bool {
let scroll = &self.conversation_scroll;
if scroll.total == 0 || scroll.visible == 0 {
return true; // Empty or not rendered yet = consider at bottom
}
// At bottom when offset + visible >= total
scroll.offset + scroll.visible >= scroll.total
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// Smooth Scrolling
// ═══════════════════════════════════════════════════════════════════════════════
impl ChatView {
/// Friction coefficient for scroll deceleration (0.0 = instant stop, 1.0 = no friction)
const SCROLL_FRICTION: f32 = 0.85;
/// Minimum velocity before stopping animation (lines per tick)
const SCROLL_MIN_VELOCITY: f32 = 0.1;
/// Initial velocity multiplier for mouse wheel (lines per scroll event)
const SCROLL_WHEEL_VELOCITY: f32 = 3.0;
/// Apply smooth scroll with initial velocity (for mouse wheel)
pub fn smooth_scroll(&mut self, direction: i8) {
// Add velocity in the scroll direction (positive = down, negative = up)
self.scroll_velocity += direction as f32 * Self::SCROLL_WHEEL_VELOCITY;
self.scroll_animating = true;
// Update user_at_bottom state based on scroll direction
if direction < 0 {
self.user_at_bottom = false; // Scrolling up = stop auto-following
}
}
/// Update scroll animation (call this every frame/tick)
/// Returns true if animation is still active
pub fn update_scroll_animation(&mut self) -> bool {
if !self.scroll_animating {
return false;
}
// Apply velocity to accumulator (sub-line precision)
self.scroll_accumulator += self.scroll_velocity;
// Convert accumulated scroll to whole lines
let lines_to_scroll = self.scroll_accumulator.trunc() as i32;
if lines_to_scroll != 0 {
self.scroll_accumulator -= lines_to_scroll as f32;
// Apply scroll based on direction
if lines_to_scroll > 0 {
for _ in 0..lines_to_scroll {
self.conversation_scroll.scroll_down();
}
} else {
for _ in 0..(-lines_to_scroll) {
self.conversation_scroll.scroll_up();
}
}
}
// Apply friction
self.scroll_velocity *= Self::SCROLL_FRICTION;
// Stop animation if velocity is negligible
if self.scroll_velocity.abs() < Self::SCROLL_MIN_VELOCITY {
self.scroll_velocity = 0.0;
self.scroll_accumulator = 0.0;
self.scroll_animating = false;
// Check if we ended up at the bottom
self.user_at_bottom = self.is_at_bottom();
return false;
}
true
}
/// Stop any ongoing smooth scroll animation
pub fn stop_smooth_scroll(&mut self) {
self.scroll_velocity = 0.0;
self.scroll_accumulator = 0.0;
self.scroll_animating = false;
}
/// Check if smooth scroll animation is active
pub fn is_scroll_animating(&self) -> bool {
self.scroll_animating
}
}