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 rows_changed(&mut self, rows: usize) {
646 self.max_offset = rows.saturating_sub(self.page_len);
647 self.offset = self.limited_offset(self.offset);
648 }
649
650 #[inline]
657 pub fn scroll_to_pos(&mut self, pos: usize) -> bool {
658 let old = self.offset;
659
660 let new = if self.page_len == 0 {
661 pos
662 } else if pos >= self.offset + self.page_len {
663 pos - self.page_len + 1
664 } else if pos < self.offset {
665 pos
666 } else {
667 old
668 };
669 self.offset = self.limited_offset(new);
670
671 old != self.offset
672 }
673
674 #[inline]
680 pub fn scroll_to_range(&mut self, range: Range<usize>) -> bool {
681 let old = self.offset;
682
683 let new = if self.page_len == 0 {
685 range.start
686 } else if range.start >= self.offset + self.page_len {
687 if range.end - range.start < self.page_len {
688 range.end - self.page_len
689 } else {
690 range.start
691 }
692 } else if range.start < self.offset {
693 range.start
694 } else if range.end >= self.offset + self.page_len {
695 if range.end - range.start < self.page_len {
696 range.end - self.page_len
697 } else {
698 range.start
699 }
700 } else {
701 old
702 };
703 self.offset = self.limited_offset(new);
704
705 old != self.offset
706 }
707
708 #[inline]
710 pub fn scroll_up(&mut self, n: usize) -> bool {
711 let old = self.offset;
712 self.offset = self.limited_offset(self.offset.saturating_sub(n));
713 old != self.offset
714 }
715
716 #[inline]
718 pub fn scroll_down(&mut self, n: usize) -> bool {
719 let old = self.offset;
720 self.offset = self.limited_offset(self.offset.saturating_add(n));
721 old != self.offset
722 }
723
724 #[inline]
726 pub fn scroll_left(&mut self, n: usize) -> bool {
727 self.scroll_up(n)
728 }
729
730 #[inline]
732 pub fn scroll_right(&mut self, n: usize) -> bool {
733 self.scroll_down(n)
734 }
735
736 #[inline]
738 pub fn limited_offset(&self, offset: usize) -> usize {
739 min(offset, self.max_offset.saturating_add(self.overscroll_by()))
740 }
741
742 #[inline]
747 pub fn max_offset(&self) -> usize {
748 self.max_offset
749 }
750
751 #[inline]
756 pub fn set_max_offset(&mut self, max: usize) {
757 self.max_offset = max;
758 }
759
760 #[inline]
762 pub fn page_len(&self) -> usize {
763 self.page_len
764 }
765
766 #[inline]
768 pub fn set_page_len(&mut self, page: usize) {
769 self.page_len = page;
770 }
771
772 #[inline]
775 pub fn scroll_by(&self) -> usize {
776 if let Some(scroll) = self.scroll_by {
777 max(scroll, 1)
778 } else {
779 max(self.page_len / 10, 1)
780 }
781 }
782
783 #[inline]
786 pub fn set_scroll_by(&mut self, scroll: Option<usize>) {
787 self.scroll_by = scroll;
788 }
789
790 #[inline]
792 pub fn overscroll_by(&self) -> usize {
793 self.overscroll_by.unwrap_or_default()
794 }
795
796 #[inline]
798 pub fn set_overscroll_by(&mut self, overscroll_by: Option<usize>) {
799 self.overscroll_by = overscroll_by;
800 }
801
802 #[inline]
804 pub fn items_added(&mut self, pos: usize, n: usize) {
805 if self.offset >= pos {
806 self.offset += n;
807 }
808 self.max_offset += n;
809 }
810
811 #[inline]
813 pub fn items_removed(&mut self, pos: usize, n: usize) {
814 if self.offset >= pos && self.offset >= n {
815 self.offset -= n;
816 }
817 self.max_offset = self.max_offset.saturating_sub(n);
818 }
819}
820
821impl ScrollState {
822 pub fn map_position_index(&self, pos: u16, base: u16, length: u16) -> usize {
827 let pos = pos.saturating_sub(base).saturating_sub(1) as usize;
829 let span = length.saturating_sub(2) as usize;
830
831 if span > 0 {
832 (self.max_offset.saturating_mul(pos)) / span
833 } else {
834 0
835 }
836 }
837}
838
839impl HandleEvent<Event, MouseOnly, ScrollOutcome> for ScrollState {
840 fn handle(&mut self, event: &Event, _qualifier: MouseOnly) -> ScrollOutcome {
841 match event {
842 ct_event!(mouse any for m) if self.mouse.drag(self.area, m) => {
843 if self.is_vertical() {
844 if m.row >= self.area.y {
845 ScrollOutcome::VPos(self.map_position_index(
846 m.row,
847 self.area.y,
848 self.area.height,
849 ))
850 } else {
851 ScrollOutcome::Unchanged
852 }
853 } else {
854 if m.column >= self.area.x {
855 ScrollOutcome::HPos(self.map_position_index(
856 m.column,
857 self.area.x,
858 self.area.width,
859 ))
860 } else {
861 ScrollOutcome::Unchanged
862 }
863 }
864 }
865 ct_event!(mouse down Left for col, row) if self.area.contains((*col, *row).into()) => {
866 if self.is_vertical() {
867 ScrollOutcome::VPos(self.map_position_index(
868 *row,
869 self.area.y,
870 self.area.height,
871 ))
872 } else {
873 ScrollOutcome::HPos(self.map_position_index(*col, self.area.x, self.area.width))
874 }
875 }
876 ct_event!(scroll down for col, row)
877 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
878 {
879 ScrollOutcome::Down(self.scroll_by())
880 }
881 ct_event!(scroll up for col, row)
882 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
883 {
884 ScrollOutcome::Up(self.scroll_by())
885 }
886 ct_event!(scroll ALT down for col, row)
888 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
889 {
890 ScrollOutcome::Right(self.scroll_by())
891 }
892 ct_event!(scroll ALT up for col, row)
894 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
895 {
896 ScrollOutcome::Left(self.scroll_by())
897 }
898 _ => ScrollOutcome::Continue,
899 }
900 }
901}