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 {
441 match self.orientation {
442 ScrollbarOrientation::VerticalRight => Padding::new(0, 1, 0, 0),
443 ScrollbarOrientation::VerticalLeft => Padding::new(1, 0, 0, 0),
444 ScrollbarOrientation::HorizontalBottom => Padding::new(0, 0, 0, 1),
445 ScrollbarOrientation::HorizontalTop => Padding::new(0, 0, 1, 0),
446 }
447 }
448}
449
450impl<'a> Scroll<'a> {
451 fn scrollbar(&self) -> Scrollbar<'a> {
453 self.scrollbar.clone()
454 }
455}
456
457impl StatefulWidget for &Scroll<'_> {
458 type State = ScrollState;
459
460 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
461 render_scroll(self, area, buf, state);
462 }
463}
464
465impl StatefulWidget for Scroll<'_> {
466 type State = ScrollState;
467
468 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
469 render_scroll(&self, area, buf, state);
470 }
471}
472
473fn render_scroll(scroll: &Scroll<'_>, area: Rect, buf: &mut Buffer, state: &mut ScrollState) {
474 state.set_orientation(scroll.orientation.clone());
475 if scroll.overscroll_by.is_some() {
476 state.set_overscroll_by(scroll.overscroll_by);
477 }
478 if scroll.scroll_by.is_some() {
479 state.set_scroll_by(scroll.scroll_by);
480 }
481 state.area = area;
482
483 if area.is_empty() {
484 return;
485 }
486
487 if state.max_offset() == 0 {
488 match scroll.policy {
489 ScrollbarPolicy::Always => {
490 scroll.scrollbar().render(
491 area,
492 buf,
493 &mut ScrollbarState::new(state.max_offset())
494 .position(state.offset())
495 .viewport_content_length(state.page_len()),
496 );
497 }
498 ScrollbarPolicy::Minimize => {
499 fill(scroll.min_symbol, scroll.min_style, area, buf);
500 }
501 ScrollbarPolicy::Collapse => {
502 }
504 }
505 } else {
506 scroll.scrollbar().render(
507 area,
508 buf,
509 &mut ScrollbarState::new(state.max_offset())
510 .position(state.offset())
511 .viewport_content_length(state.page_len()),
512 );
513 }
514}
515
516fn fill(sym: Option<&'_ str>, style: Option<Style>, area: Rect, buf: &mut Buffer) {
517 let area = buf.area.intersection(area);
518 match (sym, style) {
519 (Some(sym), Some(style)) => {
520 for y in area.top()..area.bottom() {
521 for x in area.left()..area.right() {
522 if let Some(cell) = buf.cell_mut((x, y)) {
523 cell.set_symbol(sym);
525 cell.set_style(style);
526 }
527 }
528 }
529 }
530 (None, Some(style)) => {
531 for y in area.top()..area.bottom() {
532 for x in area.left()..area.right() {
533 if let Some(cell) = buf.cell_mut((x, y)) {
534 cell.set_style(style);
536 }
537 }
538 }
539 }
540 (Some(sym), None) => {
541 for y in area.top()..area.bottom() {
542 for x in area.left()..area.right() {
543 if let Some(cell) = buf.cell_mut((x, y)) {
544 cell.set_symbol(sym);
545 }
546 }
547 }
548 }
549 (None, None) => {
550 }
552 }
553}
554
555impl Default for ScrollState {
556 fn default() -> Self {
557 Self {
558 area: Default::default(),
559 orientation: Default::default(),
560 offset: 0,
561 max_offset: 0,
562 page_len: 0,
563 scroll_by: None,
564 overscroll_by: None,
565 mouse: Default::default(),
566 non_exhaustive: NonExhaustive,
567 }
568 }
569}
570
571impl RelocatableState for ScrollState {
572 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
573 self.area = relocate_area(self.area, shift, clip);
574 }
575}
576
577impl ScrollState {
578 pub fn new() -> Self {
579 Self::default()
580 }
581
582 #[inline]
583 pub fn set_orientation(&mut self, orientation: ScrollbarOrientation) {
584 self.orientation = orientation;
585 }
586
587 #[inline]
589 pub fn is_vertical(&self) -> bool {
590 self.orientation.is_vertical()
591 }
592
593 #[inline]
595 pub fn is_horizontal(&self) -> bool {
596 self.orientation.is_horizontal()
597 }
598
599 pub fn clear(&mut self) {
601 self.offset = 0;
602 }
603
604 #[inline]
606 pub fn offset(&self) -> usize {
607 self.offset
608 }
609
610 #[inline]
615 pub fn set_offset(&mut self, offset: usize) -> bool {
616 let old = self.offset;
617 self.offset = offset;
618 old != self.offset
619 }
620
621 #[inline]
625 pub fn scroll_to_pos(&mut self, pos: usize) -> bool {
626 let old = self.offset;
627 if pos >= self.offset + self.page_len {
628 self.offset = pos - self.page_len + 1;
629 } else if pos < self.offset {
630 self.offset = pos;
631 }
632 old != self.offset
633 }
634
635 #[inline]
639 pub fn scroll_to_range(&mut self, range: Range<usize>) -> bool {
640 let old = self.offset;
641 if range.start >= self.offset + self.page_len {
643 if range.end - range.start < self.page_len {
644 self.offset = range.end - self.page_len + 1;
645 } else {
646 self.offset = range.start;
647 }
648 } else if range.start < self.offset {
649 self.offset = range.start;
650 } else if range.end >= self.offset + self.page_len {
651 if range.end - range.start < self.page_len {
652 self.offset = range.end - self.page_len + 1;
653 } else {
654 self.offset = range.start;
655 }
656 }
657 old != self.offset
658 }
659
660 #[inline]
662 pub fn scroll_up(&mut self, n: usize) -> bool {
663 let old = self.offset;
664 self.offset = self.limited_offset(self.offset.saturating_sub(n));
665 old != self.offset
666 }
667
668 #[inline]
670 pub fn scroll_down(&mut self, n: usize) -> bool {
671 let old = self.offset;
672 self.offset = self.limited_offset(self.offset.saturating_add(n));
673 old != self.offset
674 }
675
676 #[inline]
678 pub fn scroll_left(&mut self, n: usize) -> bool {
679 self.scroll_up(n)
680 }
681
682 #[inline]
684 pub fn scroll_right(&mut self, n: usize) -> bool {
685 self.scroll_down(n)
686 }
687
688 #[inline]
690 pub fn limited_offset(&self, offset: usize) -> usize {
691 min(offset, self.max_offset.saturating_add(self.overscroll_by()))
692 }
693
694 #[inline]
699 pub fn max_offset(&self) -> usize {
700 self.max_offset
701 }
702
703 #[inline]
708 pub fn set_max_offset(&mut self, max: usize) {
709 self.max_offset = max;
710 }
711
712 #[inline]
714 pub fn page_len(&self) -> usize {
715 self.page_len
716 }
717
718 #[inline]
720 pub fn set_page_len(&mut self, page: usize) {
721 self.page_len = page;
722 }
723
724 #[inline]
727 pub fn scroll_by(&self) -> usize {
728 if let Some(scroll) = self.scroll_by {
729 max(scroll, 1)
730 } else {
731 max(self.page_len / 10, 1)
732 }
733 }
734
735 #[inline]
738 pub fn set_scroll_by(&mut self, scroll: Option<usize>) {
739 self.scroll_by = scroll;
740 }
741
742 #[inline]
744 pub fn overscroll_by(&self) -> usize {
745 self.overscroll_by.unwrap_or_default()
746 }
747
748 #[inline]
750 pub fn set_overscroll_by(&mut self, overscroll_by: Option<usize>) {
751 self.overscroll_by = overscroll_by;
752 }
753
754 #[inline]
756 pub fn items_added(&mut self, pos: usize, n: usize) {
757 if self.offset >= pos {
758 self.offset += n;
759 }
760 self.max_offset += n;
761 }
762
763 #[inline]
765 pub fn items_removed(&mut self, pos: usize, n: usize) {
766 if self.offset >= pos && self.offset >= n {
767 self.offset -= n;
768 }
769 self.max_offset = self.max_offset.saturating_sub(n);
770 }
771}
772
773impl ScrollState {
774 pub fn map_position_index(&self, pos: u16, base: u16, length: u16) -> usize {
779 let pos = pos.saturating_sub(base).saturating_sub(1) as usize;
781 let span = length.saturating_sub(2) as usize;
782
783 if span > 0 {
784 (self.max_offset.saturating_mul(pos)) / span
785 } else {
786 0
787 }
788 }
789}
790
791impl HandleEvent<crossterm::event::Event, MouseOnly, ScrollOutcome> for ScrollState {
792 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> ScrollOutcome {
793 match event {
794 ct_event!(mouse any for m) if self.mouse.drag(self.area, m) => {
795 if self.is_vertical() {
796 if m.row >= self.area.y {
797 ScrollOutcome::VPos(self.map_position_index(
798 m.row,
799 self.area.y,
800 self.area.height,
801 ))
802 } else {
803 ScrollOutcome::Unchanged
804 }
805 } else {
806 if m.column >= self.area.x {
807 ScrollOutcome::HPos(self.map_position_index(
808 m.column,
809 self.area.x,
810 self.area.width,
811 ))
812 } else {
813 ScrollOutcome::Unchanged
814 }
815 }
816 }
817 ct_event!(mouse down Left for col, row) if self.area.contains((*col, *row).into()) => {
818 if self.is_vertical() {
819 ScrollOutcome::VPos(self.map_position_index(
820 *row,
821 self.area.y,
822 self.area.height,
823 ))
824 } else {
825 ScrollOutcome::HPos(self.map_position_index(*col, self.area.x, self.area.width))
826 }
827 }
828 ct_event!(scroll down for col, row)
829 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
830 {
831 ScrollOutcome::Down(self.scroll_by())
832 }
833 ct_event!(scroll up for col, row)
834 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
835 {
836 ScrollOutcome::Up(self.scroll_by())
837 }
838 ct_event!(scroll ALT down for col, row)
840 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
841 {
842 ScrollOutcome::Right(self.scroll_by())
843 }
844 ct_event!(scroll ALT up for col, row)
846 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
847 {
848 ScrollOutcome::Left(self.scroll_by())
849 }
850 _ => ScrollOutcome::Continue,
851 }
852 }
853}