1use crate::event::ScrollOutcome;
2use crate::{Scroll, ScrollState, ScrollbarPolicy};
3use rat_event::{HandleEvent, MouseOnly, ct_event, flow};
4use ratatui::buffer::Buffer;
5use ratatui::layout::{Position, Rect};
6use ratatui::style::Style;
7use ratatui::widgets::{Block, Padding, ScrollbarOrientation, StatefulWidget, Widget};
8use std::cmp::max;
9
10#[derive(Debug, Default, Clone)]
13pub struct ScrollArea<'a> {
14 style: Style,
15 ignore_block: bool,
16 block: Option<&'a Block<'a>>,
17 ignore_scroll: bool,
18 h_scroll: Option<&'a Scroll<'a>>,
19 v_scroll: Option<&'a Scroll<'a>>,
20}
21
22#[derive(Debug, Default)]
27pub struct ScrollAreaState<'a> {
28 area: Rect,
31 h_scroll: Option<&'a mut ScrollState>,
33 v_scroll: Option<&'a mut ScrollState>,
35}
36
37impl<'a> ScrollArea<'a> {
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn style(mut self, style: Style) -> Self {
44 self.style = style;
45 self
46 }
47
48 pub fn block(mut self, block: Option<&'a Block<'a>>) -> Self {
50 self.block = block;
51 self
52 }
53
54 pub fn ignore_block(mut self) -> Self {
57 self.ignore_block = true;
58 self
59 }
60
61 pub fn h_scroll(mut self, scroll: Option<&'a Scroll<'a>>) -> Self {
63 self.h_scroll = scroll;
64 self
65 }
66
67 pub fn v_scroll(mut self, scroll: Option<&'a Scroll<'a>>) -> Self {
69 self.v_scroll = scroll;
70 self
71 }
72
73 pub fn ignore_scroll(mut self) -> Self {
76 self.ignore_scroll = true;
77 self
78 }
79
80 pub fn padding(&self) -> Padding {
82 let mut padding = block_padding(&self.block);
83 if let Some(h_scroll) = self.h_scroll {
84 let scroll_pad = h_scroll.padding();
85 padding.top = max(padding.top, scroll_pad.top);
86 padding.bottom = max(padding.bottom, scroll_pad.bottom);
87 }
88 if let Some(v_scroll) = self.v_scroll {
89 let scroll_pad = v_scroll.padding();
90 padding.left = max(padding.left, scroll_pad.left);
91 padding.right = max(padding.right, scroll_pad.right);
92 }
93 padding
94 }
95
96 pub fn inner(
98 &self,
99 area: Rect,
100 hscroll_state: Option<&ScrollState>,
101 vscroll_state: Option<&ScrollState>,
102 ) -> Rect {
103 layout(
104 self.block,
105 self.h_scroll,
106 self.v_scroll,
107 area,
108 hscroll_state,
109 vscroll_state,
110 )
111 .0
112 }
113}
114
115fn block_padding(block: &Option<&Block<'_>>) -> Padding {
117 let area = Rect::new(0, 0, 20, 20);
118 let inner = if let Some(block) = block {
119 block.inner(area)
120 } else {
121 area
122 };
123 Padding {
124 left: inner.left() - area.left(),
125 right: area.right() - inner.right(),
126 top: inner.top() - area.top(),
127 bottom: area.bottom() - inner.bottom(),
128 }
129}
130
131impl<'a> StatefulWidget for ScrollArea<'a> {
132 type State = ScrollAreaState<'a>;
133
134 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
135 render_scroll_area(&self, area, buf, state);
136 }
137}
138
139impl<'a> StatefulWidget for &ScrollArea<'a> {
140 type State = ScrollAreaState<'a>;
141
142 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
143 render_scroll_area(self, area, buf, state);
144 }
145}
146
147fn render_scroll_area(
148 widget: &ScrollArea<'_>,
149 area: Rect,
150 buf: &mut Buffer,
151 state: &mut ScrollAreaState<'_>,
152) {
153 let (_, hscroll_area, vscroll_area) = layout(
154 widget.block,
155 widget.h_scroll,
156 widget.v_scroll,
157 area,
158 state.h_scroll.as_deref(),
159 state.v_scroll.as_deref(),
160 );
161
162 if !widget.ignore_block {
163 if let Some(block) = widget.block {
164 block.render(area, buf);
165 } else {
166 buf.set_style(area, widget.style);
167 }
168 }
169 if !widget.ignore_scroll {
170 if let Some(h) = widget.h_scroll {
171 if let Some(hstate) = &mut state.h_scroll {
172 h.render(hscroll_area, buf, hstate);
173 } else {
174 panic!("no horizontal scroll state");
175 }
176 }
177 if let Some(v) = widget.v_scroll {
178 if let Some(vstate) = &mut state.v_scroll {
179 v.render(vscroll_area, buf, vstate)
180 } else {
181 panic!("no vertical scroll state");
182 }
183 }
184 }
185}
186
187fn layout<'a>(
203 block: Option<&Block<'a>>,
204 hscroll: Option<&Scroll<'a>>,
205 vscroll: Option<&Scroll<'a>>,
206 area: Rect,
207 hscroll_state: Option<&ScrollState>,
208 vscroll_state: Option<&ScrollState>,
209) -> (Rect, Rect, Rect) {
210 let mut inner = area;
211
212 if let Some(block) = block {
213 inner = block.inner(area);
214 }
215
216 if let Some(hscroll) = hscroll {
217 let show = match hscroll.get_policy() {
218 ScrollbarPolicy::Always => true,
219 ScrollbarPolicy::Minimize => true,
220 ScrollbarPolicy::Collapse => {
221 if let Some(hscroll_state) = hscroll_state {
222 hscroll_state.max_offset > 0
223 } else {
224 true
225 }
226 }
227 };
228 if show {
229 match hscroll.get_orientation() {
230 ScrollbarOrientation::VerticalRight => {
231 unimplemented!(
232 "ScrollbarOrientation::VerticalRight not supported for horizontal scrolling."
233 );
234 }
235 ScrollbarOrientation::VerticalLeft => {
236 unimplemented!(
237 "ScrollbarOrientation::VerticalLeft not supported for horizontal scrolling."
238 );
239 }
240 ScrollbarOrientation::HorizontalBottom => {
241 if inner.bottom() == area.bottom() {
242 inner.height = inner.height.saturating_sub(1);
243 }
244 }
245 ScrollbarOrientation::HorizontalTop => {
246 if inner.top() == area.top() {
247 inner.y += 1;
248 inner.height = inner.height.saturating_sub(1);
249 }
250 }
251 }
252 }
253 }
254
255 if let Some(vscroll) = vscroll {
256 let show = match vscroll.get_policy() {
257 ScrollbarPolicy::Always => true,
258 ScrollbarPolicy::Minimize => true,
259 ScrollbarPolicy::Collapse => {
260 if let Some(vscroll_state) = vscroll_state {
261 vscroll_state.max_offset > 0
262 } else {
263 true
264 }
265 }
266 };
267 if show {
268 match vscroll.get_orientation() {
269 ScrollbarOrientation::VerticalRight => {
270 if inner.right() == area.right() {
271 inner.width = inner.width.saturating_sub(1);
272 }
273 }
274 ScrollbarOrientation::VerticalLeft => {
275 if inner.left() == area.left() {
276 inner.x += 1;
277 inner.width = inner.width.saturating_sub(1);
278 }
279 }
280 ScrollbarOrientation::HorizontalBottom => {
281 unimplemented!(
282 "ScrollbarOrientation::HorizontalBottom not supported for vertical scrolling."
283 );
284 }
285 ScrollbarOrientation::HorizontalTop => {
286 unimplemented!(
287 "ScrollbarOrientation::HorizontalTop not supported for vertical scrolling."
288 );
289 }
290 }
291 }
292 }
293
294 let h_area = if let Some(hscroll) = hscroll {
296 let show = match hscroll.get_policy() {
297 ScrollbarPolicy::Always => true,
298 ScrollbarPolicy::Minimize => true,
299 ScrollbarPolicy::Collapse => {
300 if let Some(hscroll_state) = hscroll_state {
301 hscroll_state.max_offset > 0
302 } else {
303 true
304 }
305 }
306 };
307 if show {
308 match hscroll.get_orientation() {
309 ScrollbarOrientation::HorizontalBottom => Rect::new(
310 inner.x + hscroll.get_start_margin(),
311 area.bottom().saturating_sub(1),
312 inner
313 .width
314 .saturating_sub(hscroll.get_start_margin() + hscroll.get_end_margin()),
315 if area.height > 0 { 1 } else { 0 },
316 ),
317 ScrollbarOrientation::HorizontalTop => Rect::new(
318 inner.x + hscroll.get_start_margin(),
319 area.y,
320 inner
321 .width
322 .saturating_sub(hscroll.get_start_margin() + hscroll.get_end_margin()),
323 if area.height > 0 { 1 } else { 0 },
324 ),
325 _ => unreachable!(),
326 }
327 } else {
328 Rect::new(area.x, area.y, 0, 0)
329 }
330 } else {
331 Rect::new(area.x, area.y, 0, 0)
332 };
333
334 let v_area = if let Some(vscroll) = vscroll {
336 let show = match vscroll.get_policy() {
337 ScrollbarPolicy::Always => true,
338 ScrollbarPolicy::Minimize => true,
339 ScrollbarPolicy::Collapse => {
340 if let Some(vscroll_state) = vscroll_state {
341 vscroll_state.max_offset > 0
342 } else {
343 true
344 }
345 }
346 };
347 if show {
348 match vscroll.get_orientation() {
349 ScrollbarOrientation::VerticalRight => Rect::new(
350 area.right().saturating_sub(1),
351 inner.y + vscroll.get_start_margin(),
352 if area.width > 0 { 1 } else { 0 },
353 inner
354 .height
355 .saturating_sub(vscroll.get_start_margin() + vscroll.get_end_margin()),
356 ),
357 ScrollbarOrientation::VerticalLeft => Rect::new(
358 area.x,
359 inner.y + vscroll.get_start_margin(),
360 if area.width > 0 { 1 } else { 0 },
361 inner
362 .height
363 .saturating_sub(vscroll.get_start_margin() + vscroll.get_end_margin()),
364 ),
365 _ => unreachable!(),
366 }
367 } else {
368 Rect::new(area.x, area.y, 0, 0)
369 }
370 } else {
371 Rect::new(area.x, area.y, 0, 0)
372 };
373
374 (inner, h_area, v_area)
375}
376
377impl<'a> ScrollAreaState<'a> {
378 pub fn new() -> Self {
379 Self::default()
380 }
381
382 pub fn area(mut self, area: Rect) -> Self {
383 self.area = area;
384 self
385 }
386
387 pub fn v_scroll(mut self, v_scroll: &'a mut ScrollState) -> Self {
388 self.v_scroll = Some(v_scroll);
389 self
390 }
391
392 pub fn v_scroll_opt(mut self, v_scroll: Option<&'a mut ScrollState>) -> Self {
393 self.v_scroll = v_scroll;
394 self
395 }
396
397 pub fn h_scroll(mut self, h_scroll: &'a mut ScrollState) -> Self {
398 self.h_scroll = Some(h_scroll);
399 self
400 }
401
402 pub fn h_scroll_opt(mut self, h_scroll: Option<&'a mut ScrollState>) -> Self {
403 self.h_scroll = h_scroll;
404 self
405 }
406}
407
408impl HandleEvent<crossterm::event::Event, MouseOnly, ScrollOutcome> for ScrollAreaState<'_> {
412 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> ScrollOutcome {
413 if let Some(h_scroll) = &mut self.h_scroll {
414 flow!(match event {
415 ct_event!(scroll ALT down for column, row) => {
417 if self.area.contains(Position::new(*column, *row)) {
418 ScrollOutcome::Right(h_scroll.scroll_by())
419 } else {
420 ScrollOutcome::Continue
421 }
422 }
423 ct_event!(scroll ALT up for column, row) => {
425 if self.area.contains(Position::new(*column, *row)) {
426 ScrollOutcome::Left(h_scroll.scroll_by())
427 } else {
428 ScrollOutcome::Continue
429 }
430 }
431 _ => ScrollOutcome::Continue,
432 });
433 flow!(h_scroll.handle(event, MouseOnly));
434 }
435 if let Some(v_scroll) = &mut self.v_scroll {
436 flow!(match event {
437 ct_event!(scroll down for column, row) => {
438 if self.area.contains(Position::new(*column, *row)) {
439 ScrollOutcome::Down(v_scroll.scroll_by())
440 } else {
441 ScrollOutcome::Continue
442 }
443 }
444 ct_event!(scroll up for column, row) => {
445 if self.area.contains(Position::new(*column, *row)) {
446 ScrollOutcome::Up(v_scroll.scroll_by())
447 } else {
448 ScrollOutcome::Continue
449 }
450 }
451 _ => ScrollOutcome::Continue,
452 });
453 flow!(v_scroll.handle(event, MouseOnly));
454 }
455
456 ScrollOutcome::Continue
457 }
458}