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 if let Some(hscroll_state) = hscroll_state {
218 let show = match hscroll.get_policy() {
219 ScrollbarPolicy::Always => true,
220 ScrollbarPolicy::Minimize => true,
221 ScrollbarPolicy::Collapse => hscroll_state.max_offset > 0,
222 };
223 if show {
224 match hscroll.get_orientation() {
225 ScrollbarOrientation::VerticalRight => {
226 unimplemented!(
227 "ScrollbarOrientation::VerticalRight not supported for horizontal scrolling."
228 );
229 }
230 ScrollbarOrientation::VerticalLeft => {
231 unimplemented!(
232 "ScrollbarOrientation::VerticalLeft not supported for horizontal scrolling."
233 );
234 }
235 ScrollbarOrientation::HorizontalBottom => {
236 if inner.bottom() == area.bottom() {
237 inner.height = inner.height.saturating_sub(1);
238 }
239 }
240 ScrollbarOrientation::HorizontalTop => {
241 if inner.top() == area.top() {
242 inner.y += 1;
243 inner.height = inner.height.saturating_sub(1);
244 }
245 }
246 }
247 }
248 } else {
249 panic!("no horizontal scroll state");
250 }
251 }
252
253 if let Some(vscroll) = vscroll {
254 if let Some(vscroll_state) = vscroll_state {
255 let show = match vscroll.get_policy() {
256 ScrollbarPolicy::Always => true,
257 ScrollbarPolicy::Minimize => true,
258 ScrollbarPolicy::Collapse => vscroll_state.max_offset > 0,
259 };
260 if show {
261 match vscroll.get_orientation() {
262 ScrollbarOrientation::VerticalRight => {
263 if inner.right() == area.right() {
264 inner.width = inner.width.saturating_sub(1);
265 }
266 }
267 ScrollbarOrientation::VerticalLeft => {
268 if inner.left() == area.left() {
269 inner.x += 1;
270 inner.width = inner.width.saturating_sub(1);
271 }
272 }
273 ScrollbarOrientation::HorizontalBottom => {
274 unimplemented!(
275 "ScrollbarOrientation::HorizontalBottom not supported for vertical scrolling."
276 );
277 }
278 ScrollbarOrientation::HorizontalTop => {
279 unimplemented!(
280 "ScrollbarOrientation::HorizontalTop not supported for vertical scrolling."
281 );
282 }
283 }
284 }
285 } else {
286 panic!("no horizontal scroll state");
287 }
288 }
289
290 let h_area = if let Some(hscroll) = hscroll {
292 if let Some(hscroll_state) = hscroll_state {
293 let show = match hscroll.get_policy() {
294 ScrollbarPolicy::Always => true,
295 ScrollbarPolicy::Minimize => true,
296 ScrollbarPolicy::Collapse => hscroll_state.max_offset > 0,
297 };
298 if show {
299 match hscroll.get_orientation() {
300 ScrollbarOrientation::HorizontalBottom => Rect::new(
301 inner.x + hscroll.get_start_margin(),
302 area.bottom().saturating_sub(1),
303 inner
304 .width
305 .saturating_sub(hscroll.get_start_margin() + hscroll.get_end_margin()),
306 if area.height > 0 { 1 } else { 0 },
307 ),
308 ScrollbarOrientation::HorizontalTop => Rect::new(
309 inner.x + hscroll.get_start_margin(),
310 area.y,
311 inner
312 .width
313 .saturating_sub(hscroll.get_start_margin() + hscroll.get_end_margin()),
314 if area.height > 0 { 1 } else { 0 },
315 ),
316 _ => unreachable!(),
317 }
318 } else {
319 Rect::new(area.x, area.y, 0, 0)
320 }
321 } else {
322 panic!("no horizontal scroll state");
323 }
324 } else {
325 Rect::new(area.x, area.y, 0, 0)
326 };
327
328 let v_area = if let Some(vscroll) = vscroll {
330 if let Some(vscroll_state) = vscroll_state {
331 let show = match vscroll.get_policy() {
332 ScrollbarPolicy::Always => true,
333 ScrollbarPolicy::Minimize => true,
334 ScrollbarPolicy::Collapse => vscroll_state.max_offset > 0,
335 };
336 if show {
337 match vscroll.get_orientation() {
338 ScrollbarOrientation::VerticalRight => Rect::new(
339 area.right().saturating_sub(1),
340 inner.y + vscroll.get_start_margin(),
341 if area.width > 0 { 1 } else { 0 },
342 inner
343 .height
344 .saturating_sub(vscroll.get_start_margin() + vscroll.get_end_margin()),
345 ),
346 ScrollbarOrientation::VerticalLeft => Rect::new(
347 area.x,
348 inner.y + vscroll.get_start_margin(),
349 if area.width > 0 { 1 } else { 0 },
350 inner
351 .height
352 .saturating_sub(vscroll.get_start_margin() + vscroll.get_end_margin()),
353 ),
354 _ => unreachable!(),
355 }
356 } else {
357 Rect::new(area.x, area.y, 0, 0)
358 }
359 } else {
360 panic!("no horizontal scroll state");
361 }
362 } else {
363 Rect::new(area.x, area.y, 0, 0)
364 };
365
366 (inner, h_area, v_area)
367}
368
369impl<'a> ScrollAreaState<'a> {
370 pub fn new() -> Self {
371 Self::default()
372 }
373
374 pub fn area(mut self, area: Rect) -> Self {
375 self.area = area;
376 self
377 }
378
379 pub fn v_scroll(mut self, v_scroll: &'a mut ScrollState) -> Self {
380 self.v_scroll = Some(v_scroll);
381 self
382 }
383
384 pub fn v_scroll_opt(mut self, v_scroll: Option<&'a mut ScrollState>) -> Self {
385 self.v_scroll = v_scroll;
386 self
387 }
388
389 pub fn h_scroll(mut self, h_scroll: &'a mut ScrollState) -> Self {
390 self.h_scroll = Some(h_scroll);
391 self
392 }
393
394 pub fn h_scroll_opt(mut self, h_scroll: Option<&'a mut ScrollState>) -> Self {
395 self.h_scroll = h_scroll;
396 self
397 }
398}
399
400impl HandleEvent<crossterm::event::Event, MouseOnly, ScrollOutcome> for ScrollAreaState<'_> {
404 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> ScrollOutcome {
405 if let Some(h_scroll) = &mut self.h_scroll {
406 flow!(match event {
407 ct_event!(scroll ALT down for column, row) => {
409 if self.area.contains(Position::new(*column, *row)) {
410 ScrollOutcome::Right(h_scroll.scroll_by())
411 } else {
412 ScrollOutcome::Continue
413 }
414 }
415 ct_event!(scroll ALT up for column, row) => {
417 if self.area.contains(Position::new(*column, *row)) {
418 ScrollOutcome::Left(h_scroll.scroll_by())
419 } else {
420 ScrollOutcome::Continue
421 }
422 }
423 _ => ScrollOutcome::Continue,
424 });
425 flow!(h_scroll.handle(event, MouseOnly));
426 }
427 if let Some(v_scroll) = &mut self.v_scroll {
428 flow!(match event {
429 ct_event!(scroll down for column, row) => {
430 if self.area.contains(Position::new(*column, *row)) {
431 ScrollOutcome::Down(v_scroll.scroll_by())
432 } else {
433 ScrollOutcome::Continue
434 }
435 }
436 ct_event!(scroll up for column, row) => {
437 if self.area.contains(Position::new(*column, *row)) {
438 ScrollOutcome::Up(v_scroll.scroll_by())
439 } else {
440 ScrollOutcome::Continue
441 }
442 }
443 _ => ScrollOutcome::Continue,
444 });
445 flow!(v_scroll.handle(event, MouseOnly));
446 }
447
448 ScrollOutcome::Continue
449 }
450}