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