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