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