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