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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
use ratatui::buffer::Buffer;
use tuirealm::{
AttrValue, Attribute, Component, Event, Frame, MockComponent, Props, State,
command::{Cmd, CmdResult},
ratatui::layout::Rect,
};
use wasmind::actors::MessageEnvelope;
use crate::tui::model::TuiMessage;
pub trait ScrollableComponentTrait<Msg, UserEvent>: Component<Msg, UserEvent>
where
Msg: PartialEq,
UserEvent: Eq + PartialEq + Clone + PartialOrd,
{
fn is_modified(&self) -> bool;
fn get_content_height(&self, area: Rect) -> u16;
}
#[derive(MockComponent)]
pub struct ScrollableComponent {
component: Scrollable,
}
impl ScrollableComponent {
pub fn new(
child: Box<dyn ScrollableComponentTrait<TuiMessage, MessageEnvelope>>,
auto_scroll: bool,
) -> Self {
Self {
component: Scrollable {
props: Props::default(),
child,
scroll_offset: 0,
content_height: 0,
cached_buffer: None,
last_render_area: None,
auto_scroll,
},
}
}
}
struct Scrollable {
props: Props,
child: Box<dyn ScrollableComponentTrait<TuiMessage, MessageEnvelope>>,
scroll_offset: u16,
content_height: u16,
cached_buffer: Option<Buffer>,
last_render_area: Option<Rect>,
auto_scroll: bool,
}
impl MockComponent for Scrollable {
fn view(&mut self, frame: &mut Frame, area: Rect) {
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
// Check if we need to re-render (child is modified or area changed)
let should_rerender = self.child.is_modified()
|| self.last_render_area != Some(area)
|| self.cached_buffer.is_none();
if should_rerender {
// Create a large temporary buffer to render the full content
let max_height = u16::MAX / 2; // Use half of u16::MAX for safety
let temp_area = Rect::new(0, 0, area.width, max_height);
let temp_buffer = Buffer::empty(temp_area);
// Swap the frame's buffer with our temporary buffer
let original_buffer = std::mem::replace(frame.buffer_mut(), temp_buffer);
// Render the child component with "infinite" height
self.child.view(frame, temp_area);
// Get back the rendered buffer and restore the original
let rendered_buffer = std::mem::replace(frame.buffer_mut(), original_buffer);
// Get the actual content height from the child component
let new_content_height = self.child.get_content_height(temp_area);
// Check if user was at bottom before content changed
let was_at_bottom = if let Some(old_area) = self.last_render_area {
let old_max_offset = self.content_height.saturating_sub(old_area.height);
self.scroll_offset >= old_max_offset
} else {
true // If no previous area, assume at bottom
};
self.content_height = new_content_height;
// Auto-scroll to bottom if user was previously at bottom and content grew
if self.auto_scroll && was_at_bottom && new_content_height > 0 {
let new_max_offset = self.content_height.saturating_sub(area.height);
self.scroll_offset = new_max_offset;
}
self.cached_buffer = Some(rendered_buffer);
self.last_render_area = Some(area);
}
// Copy the visible portion from cached buffer to the frame
if let Some(ref cached_buffer) = self.cached_buffer {
let frame_buffer = frame.buffer_mut();
// Calculate visible range
let visible_height = area
.height
.min(self.content_height.saturating_sub(self.scroll_offset));
// Optimized bulk copy using slice operations
let width = area.width as usize;
let dest_width = frame_buffer.area.width as usize;
// Process entire rows at once for better cache locality
for y in 0..visible_height {
let source_y = (self.scroll_offset + y) as usize;
let dest_y = area.y + y;
// Calculate source range in the cached buffer
let source_start = source_y * width;
let source_end = source_start + width;
// Calculate destination range in the frame buffer
let dest_start_idx = (dest_y as usize) * dest_width + (area.x as usize);
let dest_end_idx = dest_start_idx + width;
// Perform bulk copy if both ranges are valid
if let (Some(src_slice), Some(dst_slice)) = (
cached_buffer.content.get(source_start..source_end),
frame_buffer.content.get_mut(dest_start_idx..dest_end_idx),
) {
// Use clone_from_slice for efficient bulk copying
dst_slice.clone_from_slice(src_slice);
}
}
}
}
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.child.query(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.child.attr(attr, value);
}
fn state(&self) -> State {
self.child.state().clone()
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
self.child.perform(cmd)
}
}
impl Component<TuiMessage, MessageEnvelope> for ScrollableComponent {
fn on(&mut self, ev: Event<MessageEnvelope>) -> Option<TuiMessage> {
match ev.clone() {
Event::Mouse(mouse_event) => match mouse_event.kind {
tuirealm::event::MouseEventKind::ScrollDown => {
// Scroll down (increase offset)
let scroll_speed = 3; // Lines to scroll per event
let max_offset = self.component.content_height.saturating_sub(
self.component
.last_render_area
.map(|a| a.height)
.unwrap_or(0),
);
self.component.scroll_offset = self
.component
.scroll_offset
.saturating_add(scroll_speed)
.min(max_offset);
Some(TuiMessage::Redraw)
}
tuirealm::event::MouseEventKind::ScrollUp => {
// Scroll up (decrease offset)
let scroll_speed = 3; // Lines to scroll per event
self.component.scroll_offset =
self.component.scroll_offset.saturating_sub(scroll_speed);
Some(TuiMessage::Redraw)
}
_ => self.component.child.on(ev),
},
Event::Keyboard(key_event) => {
match key_event.code {
tuirealm::event::Key::Up if key_event.modifiers.is_empty() => {
// Scroll up by one line
self.component.scroll_offset =
self.component.scroll_offset.saturating_sub(1);
Some(TuiMessage::Redraw)
}
tuirealm::event::Key::Down if key_event.modifiers.is_empty() => {
// Scroll down by one line
let max_offset = self.component.content_height.saturating_sub(
self.component
.last_render_area
.map(|a| a.height)
.unwrap_or(0),
);
self.component.scroll_offset = self
.component
.scroll_offset
.saturating_add(1)
.min(max_offset);
Some(TuiMessage::Redraw)
}
tuirealm::event::Key::PageDown => {
// Scroll down by page
let page_size = self
.component
.last_render_area
.map(|a| a.height)
.unwrap_or(10);
let max_offset = self.component.content_height.saturating_sub(page_size);
self.component.scroll_offset = self
.component
.scroll_offset
.saturating_add(page_size)
.min(max_offset);
Some(TuiMessage::Redraw)
}
tuirealm::event::Key::PageUp => {
// Scroll up by page
let page_size = self
.component
.last_render_area
.map(|a| a.height)
.unwrap_or(10);
self.component.scroll_offset =
self.component.scroll_offset.saturating_sub(page_size);
Some(TuiMessage::Redraw)
}
tuirealm::event::Key::CtrlHome => {
// Jump to top
self.component.scroll_offset = 0;
Some(TuiMessage::Redraw)
}
tuirealm::event::Key::CtrlEnd => {
// Jump to bottom
let max_offset = self.component.content_height.saturating_sub(
self.component
.last_render_area
.map(|a| a.height)
.unwrap_or(0),
);
self.component.scroll_offset = max_offset;
Some(TuiMessage::Redraw)
}
_ => self.component.child.on(ev),
}
}
_ => self.component.child.on(ev),
}
}
}