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