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