1use crate::_private::NonExhaustive;
2use crate::ScrollbarPolicy;
3use crate::event::ScrollOutcome;
4use rat_event::util::MouseFlags;
5use rat_event::{HandleEvent, MouseOnly, ct_event};
6use rat_reloc::RelocatableState;
7use ratatui_core::buffer::Buffer;
8use ratatui_core::layout::Rect;
9use ratatui_core::style::Style;
10use ratatui_core::symbols;
11use ratatui_core::widgets::StatefulWidget;
12use ratatui_crossterm::crossterm::event::Event;
13use ratatui_widgets::block::Padding;
14use ratatui_widgets::scrollbar::{Scrollbar, ScrollbarOrientation, ScrollbarState};
15use std::cmp::{max, min};
16use std::mem;
17use std::ops::Range;
18
19#[derive(Debug, Default, Clone)]
25pub struct Scroll<'a> {
26 policy: ScrollbarPolicy,
27 orientation: ScrollbarOrientation,
28
29 start_margin: u16,
30 end_margin: u16,
31 overscroll_by: Option<usize>,
32 scroll_by: Option<usize>,
33
34 scrollbar: Scrollbar<'a>,
35 min_style: Option<Style>,
36 min_symbol: Option<&'a str>,
37 hor_symbols: Option<ScrollSymbols>,
38 ver_symbols: Option<ScrollSymbols>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ScrollState {
63 pub area: Rect,
66 pub orientation: ScrollbarOrientation,
69
70 pub offset: usize,
73 pub page_len: usize,
77 pub max_offset: usize,
84
85 pub scroll_by: Option<usize>,
90 pub overscroll_by: Option<usize>,
95
96 pub mouse: MouseFlags,
99
100 pub non_exhaustive: NonExhaustive,
101}
102
103#[derive(Debug, Clone)]
105pub struct ScrollStyle {
106 pub thumb_style: Option<Style>,
107 pub track_style: Option<Style>,
108 pub begin_style: Option<Style>,
109 pub end_style: Option<Style>,
110 pub min_style: Option<Style>,
111
112 pub horizontal: Option<ScrollSymbols>,
113 pub vertical: Option<ScrollSymbols>,
114
115 pub policy: Option<ScrollbarPolicy>,
116
117 pub non_exhaustive: NonExhaustive,
118}
119
120#[derive(Debug, Clone, Copy)]
141pub struct ScrollSymbols {
142 pub track: &'static str,
143 pub thumb: &'static str,
144 pub begin: &'static str,
145 pub end: &'static str,
146 pub min: &'static str,
147}
148
149pub const SCROLLBAR_DOUBLE_VERTICAL: ScrollSymbols = ScrollSymbols {
150 track: symbols::line::DOUBLE_VERTICAL,
151 thumb: symbols::block::FULL,
152 begin: "▲",
153 end: "▼",
154 min: symbols::line::DOUBLE_VERTICAL,
155};
156
157pub const SCROLLBAR_DOUBLE_HORIZONTAL: ScrollSymbols = ScrollSymbols {
158 track: symbols::line::DOUBLE_HORIZONTAL,
159 thumb: symbols::block::FULL,
160 begin: "◄",
161 end: "►",
162 min: symbols::line::DOUBLE_HORIZONTAL,
163};
164
165pub const SCROLLBAR_VERTICAL: ScrollSymbols = ScrollSymbols {
166 track: symbols::line::VERTICAL,
167 thumb: symbols::block::FULL,
168 begin: "↑",
169 end: "↓",
170 min: symbols::line::VERTICAL,
171};
172
173pub const SCROLLBAR_HORIZONTAL: ScrollSymbols = ScrollSymbols {
174 track: symbols::line::HORIZONTAL,
175 thumb: symbols::block::FULL,
176 begin: "←",
177 end: "→",
178 min: symbols::line::HORIZONTAL,
179};
180
181impl From<&'_ ScrollSymbols> for symbols::scrollbar::Set<'_> {
182 fn from(value: &'_ ScrollSymbols) -> Self {
183 symbols::scrollbar::Set {
184 track: value.track,
185 thumb: value.thumb,
186 begin: value.begin,
187 end: value.end,
188 }
189 }
190}
191
192impl Default for ScrollStyle {
193 fn default() -> Self {
194 Self {
195 thumb_style: None,
196 track_style: None,
197 begin_style: None,
198 end_style: None,
199 min_style: None,
200 horizontal: None,
201 vertical: None,
202 policy: None,
203 non_exhaustive: NonExhaustive,
204 }
205 }
206}
207
208impl<'a> Scroll<'a> {
209 pub fn new() -> Self {
210 Self::default()
211 }
212
213 pub fn horizontal() -> Self {
215 Self::default().orientation(ScrollbarOrientation::HorizontalBottom)
216 }
217
218 pub fn vertical() -> Self {
220 Self::default().orientation(ScrollbarOrientation::VerticalRight)
221 }
222
223 pub fn policy(mut self, policy: ScrollbarPolicy) -> Self {
225 self.policy = policy;
226 self
227 }
228
229 pub fn get_policy(&self) -> ScrollbarPolicy {
231 self.policy
232 }
233
234 pub fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
236 if self.orientation != orientation {
237 self.orientation = orientation.clone();
238 self.scrollbar = self.scrollbar.orientation(orientation);
239 self.update_symbols();
240 }
241 self
242 }
243
244 pub fn get_orientation(&self) -> ScrollbarOrientation {
246 self.orientation.clone()
247 }
248
249 pub fn override_vertical(mut self) -> Self {
253 let orientation = match self.orientation {
254 ScrollbarOrientation::VerticalRight => ScrollbarOrientation::VerticalRight,
255 ScrollbarOrientation::VerticalLeft => ScrollbarOrientation::VerticalLeft,
256 ScrollbarOrientation::HorizontalBottom => ScrollbarOrientation::VerticalRight,
257 ScrollbarOrientation::HorizontalTop => ScrollbarOrientation::VerticalRight,
258 };
259 if self.orientation != orientation {
260 self.orientation = orientation.clone();
261 self.scrollbar = self.scrollbar.orientation(orientation);
262 self.update_symbols();
263 }
264 self
265 }
266
267 pub fn override_horizontal(mut self) -> Self {
271 let orientation = match self.orientation {
272 ScrollbarOrientation::VerticalRight => ScrollbarOrientation::HorizontalBottom,
273 ScrollbarOrientation::VerticalLeft => ScrollbarOrientation::HorizontalBottom,
274 ScrollbarOrientation::HorizontalBottom => ScrollbarOrientation::HorizontalBottom,
275 ScrollbarOrientation::HorizontalTop => ScrollbarOrientation::HorizontalTop,
276 };
277 if self.orientation != orientation {
278 self.orientation = orientation.clone();
279 self.scrollbar = self.scrollbar.orientation(orientation);
280 self.update_symbols();
281 }
282 self
283 }
284
285 pub fn is_vertical(&self) -> bool {
287 match self.orientation {
288 ScrollbarOrientation::VerticalRight => true,
289 ScrollbarOrientation::VerticalLeft => true,
290 ScrollbarOrientation::HorizontalBottom => false,
291 ScrollbarOrientation::HorizontalTop => false,
292 }
293 }
294
295 pub fn is_horizontal(&self) -> bool {
297 match self.orientation {
298 ScrollbarOrientation::VerticalRight => false,
299 ScrollbarOrientation::VerticalLeft => false,
300 ScrollbarOrientation::HorizontalBottom => true,
301 ScrollbarOrientation::HorizontalTop => true,
302 }
303 }
304
305 pub fn start_margin(mut self, start_margin: u16) -> Self {
307 self.start_margin = start_margin;
308 self
309 }
310
311 pub fn get_start_margin(&self) -> u16 {
313 self.start_margin
314 }
315
316 pub fn end_margin(mut self, end_margin: u16) -> Self {
318 self.end_margin = end_margin;
319 self
320 }
321
322 pub fn get_end_margin(&self) -> u16 {
324 self.end_margin
325 }
326
327 pub fn overscroll_by(mut self, overscroll: usize) -> Self {
329 self.overscroll_by = Some(overscroll);
330 self
331 }
332
333 pub fn scroll_by(mut self, scroll: usize) -> Self {
335 self.scroll_by = Some(scroll);
336 self
337 }
338
339 pub fn styles(mut self, styles: ScrollStyle) -> Self {
341 if let Some(horizontal) = styles.horizontal {
342 self.hor_symbols = Some(horizontal);
343 }
344 if let Some(vertical) = styles.vertical {
345 self.ver_symbols = Some(vertical);
346 }
347 self.update_symbols();
348
349 if let Some(thumb_style) = styles.thumb_style {
350 self.scrollbar = self.scrollbar.thumb_style(thumb_style);
351 }
352 if let Some(track_style) = styles.track_style {
353 self.scrollbar = self.scrollbar.track_style(track_style);
354 }
355 if let Some(begin_style) = styles.begin_style {
356 self.scrollbar = self.scrollbar.begin_style(begin_style);
357 }
358 if let Some(end_style) = styles.end_style {
359 self.scrollbar = self.scrollbar.end_style(end_style);
360 }
361 if styles.min_style.is_some() {
362 self.min_style = styles.min_style;
363 }
364 if let Some(policy) = styles.policy {
365 self.policy = policy;
366 }
367 self
368 }
369
370 fn update_symbols(&mut self) {
371 if self.is_horizontal() {
372 if let Some(horizontal) = &self.hor_symbols {
373 self.min_symbol = Some(horizontal.min);
374 self.scrollbar = mem::take(&mut self.scrollbar).symbols(horizontal.into());
375 }
376 } else {
377 if let Some(vertical) = &self.ver_symbols {
378 self.min_symbol = Some(vertical.min);
379 self.scrollbar = mem::take(&mut self.scrollbar).symbols(vertical.into());
380 }
381 }
382 }
383
384 pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
386 self.scrollbar = self.scrollbar.thumb_symbol(thumb_symbol);
387 self
388 }
389
390 pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
392 self.scrollbar = self.scrollbar.thumb_style(thumb_style);
393 self
394 }
395
396 pub fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
398 self.scrollbar = self.scrollbar.track_symbol(track_symbol);
399 self
400 }
401
402 pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
404 self.scrollbar = self.scrollbar.track_style(track_style);
405 self
406 }
407
408 pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
410 self.scrollbar = self.scrollbar.begin_symbol(begin_symbol);
411 self
412 }
413
414 pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
416 self.scrollbar = self.scrollbar.begin_style(begin_style);
417 self
418 }
419
420 pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
422 self.scrollbar = self.scrollbar.end_symbol(end_symbol);
423 self
424 }
425
426 pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
428 self.scrollbar = self.scrollbar.end_style(end_style);
429 self
430 }
431
432 pub fn min_symbol(mut self, min_symbol: Option<&'a str>) -> Self {
434 self.min_symbol = min_symbol;
435 self
436 }
437
438 pub fn min_style<S: Into<Style>>(mut self, min_style: S) -> Self {
440 self.min_style = Some(min_style.into());
441 self
442 }
443
444 pub fn symbols(mut self, symbols: &ScrollSymbols) -> Self {
446 self.min_symbol = Some(symbols.min);
447 self.scrollbar = self.scrollbar.symbols(symbols.into());
448 self
449 }
450
451 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
453 let style = style.into();
454 self.min_style = Some(style);
455 self.scrollbar = self.scrollbar.style(style);
456 self
457 }
458
459 pub fn padding(&self) -> Padding {
461 match self.orientation {
462 ScrollbarOrientation::VerticalRight => Padding::new(0, 1, 0, 0),
463 ScrollbarOrientation::VerticalLeft => Padding::new(1, 0, 0, 0),
464 ScrollbarOrientation::HorizontalBottom => Padding::new(0, 0, 0, 1),
465 ScrollbarOrientation::HorizontalTop => Padding::new(0, 0, 1, 0),
466 }
467 }
468}
469
470impl<'a> Scroll<'a> {
471 fn scrollbar(&self) -> Scrollbar<'a> {
473 self.scrollbar.clone()
474 }
475}
476
477impl StatefulWidget for &Scroll<'_> {
478 type State = ScrollState;
479
480 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
481 render_scroll(self, area, buf, state);
482 }
483}
484
485impl StatefulWidget for Scroll<'_> {
486 type State = ScrollState;
487
488 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
489 render_scroll(&self, area, buf, state);
490 }
491}
492
493fn render_scroll(scroll: &Scroll<'_>, area: Rect, buf: &mut Buffer, state: &mut ScrollState) {
494 state.set_orientation(scroll.orientation.clone());
495 if scroll.overscroll_by.is_some() {
496 state.set_overscroll_by(scroll.overscroll_by);
497 }
498 if scroll.scroll_by.is_some() {
499 state.set_scroll_by(scroll.scroll_by);
500 }
501 state.area = area;
502
503 if area.is_empty() {
504 return;
505 }
506
507 if state.max_offset() == 0 {
508 match scroll.policy {
509 ScrollbarPolicy::Always => {
510 scroll.scrollbar().render(
511 area,
512 buf,
513 &mut ScrollbarState::new(1)
514 .position(state.offset())
515 .viewport_content_length(state.page_len()),
516 );
517 }
518 ScrollbarPolicy::Minimize => {
519 fill(scroll.min_symbol, scroll.min_style, area, buf);
520 }
521 ScrollbarPolicy::Collapse => {
522 }
524 }
525 } else {
526 scroll.scrollbar().render(
527 area,
528 buf,
529 &mut ScrollbarState::new(state.max_offset())
530 .position(state.offset())
531 .viewport_content_length(state.page_len()),
532 );
533 }
534}
535
536fn fill(sym: Option<&'_ str>, style: Option<Style>, area: Rect, buf: &mut Buffer) {
537 let area = buf.area.intersection(area);
538 match (sym, style) {
539 (Some(sym), Some(style)) => {
540 for y in area.top()..area.bottom() {
541 for x in area.left()..area.right() {
542 if let Some(cell) = buf.cell_mut((x, y)) {
543 cell.set_symbol(sym);
545 cell.set_style(style);
546 }
547 }
548 }
549 }
550 (None, Some(style)) => {
551 for y in area.top()..area.bottom() {
552 for x in area.left()..area.right() {
553 if let Some(cell) = buf.cell_mut((x, y)) {
554 cell.set_style(style);
556 }
557 }
558 }
559 }
560 (Some(sym), None) => {
561 for y in area.top()..area.bottom() {
562 for x in area.left()..area.right() {
563 if let Some(cell) = buf.cell_mut((x, y)) {
564 cell.set_symbol(sym);
565 }
566 }
567 }
568 }
569 (None, None) => {
570 }
572 }
573}
574
575impl Default for ScrollState {
576 fn default() -> Self {
577 Self {
578 area: Default::default(),
579 orientation: Default::default(),
580 offset: 0,
581 max_offset: 0,
582 page_len: 0,
583 scroll_by: None,
584 overscroll_by: None,
585 mouse: Default::default(),
586 non_exhaustive: NonExhaustive,
587 }
588 }
589}
590
591impl RelocatableState for ScrollState {
592 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
593 self.area.relocate(shift, clip);
594 }
595}
596
597impl ScrollState {
598 pub fn new() -> Self {
599 Self::default()
600 }
601
602 #[inline]
603 pub fn set_orientation(&mut self, orientation: ScrollbarOrientation) {
604 self.orientation = orientation;
605 }
606
607 #[inline]
609 pub fn is_vertical(&self) -> bool {
610 self.orientation.is_vertical()
611 }
612
613 #[inline]
615 pub fn is_horizontal(&self) -> bool {
616 self.orientation.is_horizontal()
617 }
618
619 pub fn clear(&mut self) {
621 self.offset = 0;
622 }
623
624 #[inline]
626 pub fn offset(&self) -> usize {
627 self.offset
628 }
629
630 #[inline]
635 pub fn set_offset(&mut self, offset: usize) -> bool {
636 let old = self.offset;
637 self.offset = offset;
638 old != self.offset
639 }
640
641 #[inline]
645 pub fn scroll_to_pos(&mut self, pos: usize) -> bool {
646 let old = self.offset;
647 if pos >= self.offset + self.page_len {
648 self.offset = pos - self.page_len + 1;
649 } else if pos < self.offset {
650 self.offset = pos;
651 }
652 old != self.offset
653 }
654
655 #[inline]
659 pub fn scroll_to_range(&mut self, range: Range<usize>) -> bool {
660 let old = self.offset;
661 if range.start >= self.offset + self.page_len {
663 if range.end - range.start < self.page_len {
664 self.offset = range.end - self.page_len;
665 } else {
666 self.offset = range.start;
667 }
668 } else if range.start < self.offset {
669 self.offset = range.start;
670 } else if range.end >= self.offset + self.page_len {
671 if range.end - range.start < self.page_len {
672 self.offset = range.end - self.page_len;
673 } else {
674 self.offset = range.start;
675 }
676 }
677 old != self.offset
678 }
679
680 #[inline]
682 pub fn scroll_up(&mut self, n: usize) -> bool {
683 let old = self.offset;
684 self.offset = self.limited_offset(self.offset.saturating_sub(n));
685 old != self.offset
686 }
687
688 #[inline]
690 pub fn scroll_down(&mut self, n: usize) -> bool {
691 let old = self.offset;
692 self.offset = self.limited_offset(self.offset.saturating_add(n));
693 old != self.offset
694 }
695
696 #[inline]
698 pub fn scroll_left(&mut self, n: usize) -> bool {
699 self.scroll_up(n)
700 }
701
702 #[inline]
704 pub fn scroll_right(&mut self, n: usize) -> bool {
705 self.scroll_down(n)
706 }
707
708 #[inline]
710 pub fn limited_offset(&self, offset: usize) -> usize {
711 min(offset, self.max_offset.saturating_add(self.overscroll_by()))
712 }
713
714 #[inline]
719 pub fn max_offset(&self) -> usize {
720 self.max_offset
721 }
722
723 #[inline]
728 pub fn set_max_offset(&mut self, max: usize) {
729 self.max_offset = max;
730 }
731
732 #[inline]
734 pub fn page_len(&self) -> usize {
735 self.page_len
736 }
737
738 #[inline]
740 pub fn set_page_len(&mut self, page: usize) {
741 self.page_len = page;
742 }
743
744 #[inline]
747 pub fn scroll_by(&self) -> usize {
748 if let Some(scroll) = self.scroll_by {
749 max(scroll, 1)
750 } else {
751 max(self.page_len / 10, 1)
752 }
753 }
754
755 #[inline]
758 pub fn set_scroll_by(&mut self, scroll: Option<usize>) {
759 self.scroll_by = scroll;
760 }
761
762 #[inline]
764 pub fn overscroll_by(&self) -> usize {
765 self.overscroll_by.unwrap_or_default()
766 }
767
768 #[inline]
770 pub fn set_overscroll_by(&mut self, overscroll_by: Option<usize>) {
771 self.overscroll_by = overscroll_by;
772 }
773
774 #[inline]
776 pub fn items_added(&mut self, pos: usize, n: usize) {
777 if self.offset >= pos {
778 self.offset += n;
779 }
780 self.max_offset += n;
781 }
782
783 #[inline]
785 pub fn items_removed(&mut self, pos: usize, n: usize) {
786 if self.offset >= pos && self.offset >= n {
787 self.offset -= n;
788 }
789 self.max_offset = self.max_offset.saturating_sub(n);
790 }
791}
792
793impl ScrollState {
794 pub fn map_position_index(&self, pos: u16, base: u16, length: u16) -> usize {
799 let pos = pos.saturating_sub(base).saturating_sub(1) as usize;
801 let span = length.saturating_sub(2) as usize;
802
803 if span > 0 {
804 (self.max_offset.saturating_mul(pos)) / span
805 } else {
806 0
807 }
808 }
809}
810
811impl HandleEvent<Event, MouseOnly, ScrollOutcome> for ScrollState {
812 fn handle(&mut self, event: &Event, _qualifier: MouseOnly) -> ScrollOutcome {
813 match event {
814 ct_event!(mouse any for m) if self.mouse.drag(self.area, m) => {
815 if self.is_vertical() {
816 if m.row >= self.area.y {
817 ScrollOutcome::VPos(self.map_position_index(
818 m.row,
819 self.area.y,
820 self.area.height,
821 ))
822 } else {
823 ScrollOutcome::Unchanged
824 }
825 } else {
826 if m.column >= self.area.x {
827 ScrollOutcome::HPos(self.map_position_index(
828 m.column,
829 self.area.x,
830 self.area.width,
831 ))
832 } else {
833 ScrollOutcome::Unchanged
834 }
835 }
836 }
837 ct_event!(mouse down Left for col, row) if self.area.contains((*col, *row).into()) => {
838 if self.is_vertical() {
839 ScrollOutcome::VPos(self.map_position_index(
840 *row,
841 self.area.y,
842 self.area.height,
843 ))
844 } else {
845 ScrollOutcome::HPos(self.map_position_index(*col, self.area.x, self.area.width))
846 }
847 }
848 ct_event!(scroll down for col, row)
849 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
850 {
851 ScrollOutcome::Down(self.scroll_by())
852 }
853 ct_event!(scroll up for col, row)
854 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
855 {
856 ScrollOutcome::Up(self.scroll_by())
857 }
858 ct_event!(scroll ALT down for col, row)
860 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
861 {
862 ScrollOutcome::Right(self.scroll_by())
863 }
864 ct_event!(scroll ALT up for col, row)
866 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
867 {
868 ScrollOutcome::Left(self.scroll_by())
869 }
870 _ => ScrollOutcome::Continue,
871 }
872 }
873}