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, relocate_area};
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 horizontal() -> Self {
209 Self::default().orientation(ScrollbarOrientation::HorizontalBottom)
210 }
211
212 pub fn vertical() -> Self {
214 Self::default().orientation(ScrollbarOrientation::VerticalRight)
215 }
216
217 pub fn policy(mut self, policy: ScrollbarPolicy) -> Self {
219 self.policy = policy;
220 self
221 }
222
223 pub fn get_policy(&self) -> ScrollbarPolicy {
225 self.policy
226 }
227
228 pub fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
230 if self.orientation != orientation {
231 self.orientation = orientation.clone();
232 self.scrollbar = self.scrollbar.orientation(orientation);
233 self.update_symbols();
234 }
235 self
236 }
237
238 pub fn get_orientation(&self) -> ScrollbarOrientation {
240 self.orientation.clone()
241 }
242
243 pub fn override_vertical(mut self) -> Self {
247 let orientation = match self.orientation {
248 ScrollbarOrientation::VerticalRight => ScrollbarOrientation::VerticalRight,
249 ScrollbarOrientation::VerticalLeft => ScrollbarOrientation::VerticalLeft,
250 ScrollbarOrientation::HorizontalBottom => ScrollbarOrientation::VerticalRight,
251 ScrollbarOrientation::HorizontalTop => ScrollbarOrientation::VerticalRight,
252 };
253 if self.orientation != orientation {
254 self.orientation = orientation.clone();
255 self.scrollbar = self.scrollbar.orientation(orientation);
256 self.update_symbols();
257 }
258 self
259 }
260
261 pub fn override_horizontal(mut self) -> Self {
265 let orientation = match self.orientation {
266 ScrollbarOrientation::VerticalRight => ScrollbarOrientation::HorizontalBottom,
267 ScrollbarOrientation::VerticalLeft => ScrollbarOrientation::HorizontalBottom,
268 ScrollbarOrientation::HorizontalBottom => ScrollbarOrientation::HorizontalBottom,
269 ScrollbarOrientation::HorizontalTop => ScrollbarOrientation::HorizontalTop,
270 };
271 if self.orientation != orientation {
272 self.orientation = orientation.clone();
273 self.scrollbar = self.scrollbar.orientation(orientation);
274 self.update_symbols();
275 }
276 self
277 }
278
279 pub fn is_vertical(&self) -> bool {
281 match self.orientation {
282 ScrollbarOrientation::VerticalRight => true,
283 ScrollbarOrientation::VerticalLeft => true,
284 ScrollbarOrientation::HorizontalBottom => false,
285 ScrollbarOrientation::HorizontalTop => false,
286 }
287 }
288
289 pub fn is_horizontal(&self) -> bool {
291 match self.orientation {
292 ScrollbarOrientation::VerticalRight => false,
293 ScrollbarOrientation::VerticalLeft => false,
294 ScrollbarOrientation::HorizontalBottom => true,
295 ScrollbarOrientation::HorizontalTop => true,
296 }
297 }
298
299 pub fn start_margin(mut self, start_margin: u16) -> Self {
301 self.start_margin = start_margin;
302 self
303 }
304
305 pub fn get_start_margin(&self) -> u16 {
307 self.start_margin
308 }
309
310 pub fn end_margin(mut self, end_margin: u16) -> Self {
312 self.end_margin = end_margin;
313 self
314 }
315
316 pub fn get_end_margin(&self) -> u16 {
318 self.end_margin
319 }
320
321 pub fn overscroll_by(mut self, overscroll: usize) -> Self {
323 self.overscroll_by = Some(overscroll);
324 self
325 }
326
327 pub fn scroll_by(mut self, scroll: usize) -> Self {
329 self.scroll_by = Some(scroll);
330 self
331 }
332
333 pub fn styles(mut self, styles: ScrollStyle) -> Self {
335 if let Some(horizontal) = styles.horizontal {
336 self.hor_symbols = Some(horizontal);
337 }
338 if let Some(vertical) = styles.vertical {
339 self.ver_symbols = Some(vertical);
340 }
341 self.update_symbols();
342
343 if let Some(thumb_style) = styles.thumb_style {
344 self.scrollbar = self.scrollbar.thumb_style(thumb_style);
345 }
346 if let Some(track_style) = styles.track_style {
347 self.scrollbar = self.scrollbar.track_style(track_style);
348 }
349 if let Some(begin_style) = styles.begin_style {
350 self.scrollbar = self.scrollbar.begin_style(begin_style);
351 }
352 if let Some(end_style) = styles.end_style {
353 self.scrollbar = self.scrollbar.end_style(end_style);
354 }
355 if styles.min_style.is_some() {
356 self.min_style = styles.min_style;
357 }
358 self
359 }
360
361 fn update_symbols(&mut self) {
362 if self.is_horizontal() {
363 if let Some(horizontal) = &self.hor_symbols {
364 self.min_symbol = Some(horizontal.min);
365 self.scrollbar = mem::take(&mut self.scrollbar).symbols(horizontal.into());
366 }
367 } else {
368 if let Some(vertical) = &self.ver_symbols {
369 self.min_symbol = Some(vertical.min);
370 self.scrollbar = mem::take(&mut self.scrollbar).symbols(vertical.into());
371 }
372 }
373 }
374
375 pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
377 self.scrollbar = self.scrollbar.thumb_symbol(thumb_symbol);
378 self
379 }
380
381 pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
383 self.scrollbar = self.scrollbar.thumb_style(thumb_style);
384 self
385 }
386
387 pub fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
389 self.scrollbar = self.scrollbar.track_symbol(track_symbol);
390 self
391 }
392
393 pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
395 self.scrollbar = self.scrollbar.track_style(track_style);
396 self
397 }
398
399 pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
401 self.scrollbar = self.scrollbar.begin_symbol(begin_symbol);
402 self
403 }
404
405 pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
407 self.scrollbar = self.scrollbar.begin_style(begin_style);
408 self
409 }
410
411 pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
413 self.scrollbar = self.scrollbar.end_symbol(end_symbol);
414 self
415 }
416
417 pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
419 self.scrollbar = self.scrollbar.end_style(end_style);
420 self
421 }
422
423 pub fn min_symbol(mut self, min_symbol: Option<&'a str>) -> Self {
425 self.min_symbol = min_symbol;
426 self
427 }
428
429 pub fn min_style<S: Into<Style>>(mut self, min_style: S) -> Self {
431 self.min_style = Some(min_style.into());
432 self
433 }
434
435 pub fn symbols(mut self, symbols: &ScrollSymbols) -> Self {
437 self.min_symbol = Some(symbols.min);
438 self.scrollbar = self.scrollbar.symbols(symbols.into());
439 self
440 }
441
442 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
444 let style = style.into();
445 self.min_style = Some(style);
446 self.scrollbar = self.scrollbar.style(style);
447 self
448 }
449
450 pub fn padding(&self) -> Padding {
452 match self.orientation {
453 ScrollbarOrientation::VerticalRight => Padding::new(0, 1, 0, 0),
454 ScrollbarOrientation::VerticalLeft => Padding::new(1, 0, 0, 0),
455 ScrollbarOrientation::HorizontalBottom => Padding::new(0, 0, 0, 1),
456 ScrollbarOrientation::HorizontalTop => Padding::new(0, 0, 1, 0),
457 }
458 }
459}
460
461impl<'a> Scroll<'a> {
462 fn scrollbar(&self) -> Scrollbar<'a> {
464 self.scrollbar.clone()
465 }
466}
467
468impl StatefulWidget for &Scroll<'_> {
469 type State = ScrollState;
470
471 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
472 render_scroll(self, area, buf, state);
473 }
474}
475
476impl StatefulWidget for Scroll<'_> {
477 type State = ScrollState;
478
479 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
480 render_scroll(&self, area, buf, state);
481 }
482}
483
484fn render_scroll(scroll: &Scroll<'_>, area: Rect, buf: &mut Buffer, state: &mut ScrollState) {
485 state.set_orientation(scroll.orientation.clone());
486 if scroll.overscroll_by.is_some() {
487 state.set_overscroll_by(scroll.overscroll_by);
488 }
489 if scroll.scroll_by.is_some() {
490 state.set_scroll_by(scroll.scroll_by);
491 }
492 state.area = area;
493
494 if area.is_empty() {
495 return;
496 }
497
498 if state.max_offset() == 0 {
499 match scroll.policy {
500 ScrollbarPolicy::Always => {
501 scroll.scrollbar().render(
502 area,
503 buf,
504 &mut ScrollbarState::new(1)
505 .position(state.offset())
506 .viewport_content_length(state.page_len()),
507 );
508 }
509 ScrollbarPolicy::Minimize => {
510 fill(scroll.min_symbol, scroll.min_style, area, buf);
511 }
512 ScrollbarPolicy::Collapse => {
513 }
515 }
516 } else {
517 scroll.scrollbar().render(
518 area,
519 buf,
520 &mut ScrollbarState::new(state.max_offset())
521 .position(state.offset())
522 .viewport_content_length(state.page_len()),
523 );
524 }
525}
526
527fn fill(sym: Option<&'_ str>, style: Option<Style>, area: Rect, buf: &mut Buffer) {
528 let area = buf.area.intersection(area);
529 match (sym, style) {
530 (Some(sym), 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_symbol(sym);
536 cell.set_style(style);
537 }
538 }
539 }
540 }
541 (None, Some(style)) => {
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_style(style);
547 }
548 }
549 }
550 }
551 (Some(sym), None) => {
552 for y in area.top()..area.bottom() {
553 for x in area.left()..area.right() {
554 if let Some(cell) = buf.cell_mut((x, y)) {
555 cell.set_symbol(sym);
556 }
557 }
558 }
559 }
560 (None, None) => {
561 }
563 }
564}
565
566impl Default for ScrollState {
567 fn default() -> Self {
568 Self {
569 area: Default::default(),
570 orientation: Default::default(),
571 offset: 0,
572 max_offset: 0,
573 page_len: 0,
574 scroll_by: None,
575 overscroll_by: None,
576 mouse: Default::default(),
577 non_exhaustive: NonExhaustive,
578 }
579 }
580}
581
582impl RelocatableState for ScrollState {
583 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
584 self.area = relocate_area(self.area, shift, clip);
585 }
586}
587
588impl ScrollState {
589 pub fn new() -> Self {
590 Self::default()
591 }
592
593 #[inline]
594 pub fn set_orientation(&mut self, orientation: ScrollbarOrientation) {
595 self.orientation = orientation;
596 }
597
598 #[inline]
600 pub fn is_vertical(&self) -> bool {
601 self.orientation.is_vertical()
602 }
603
604 #[inline]
606 pub fn is_horizontal(&self) -> bool {
607 self.orientation.is_horizontal()
608 }
609
610 pub fn clear(&mut self) {
612 self.offset = 0;
613 }
614
615 #[inline]
617 pub fn offset(&self) -> usize {
618 self.offset
619 }
620
621 #[inline]
626 pub fn set_offset(&mut self, offset: usize) -> bool {
627 let old = self.offset;
628 self.offset = offset;
629 old != self.offset
630 }
631
632 #[inline]
636 pub fn scroll_to_pos(&mut self, pos: usize) -> bool {
637 let old = self.offset;
638 if pos >= self.offset + self.page_len {
639 self.offset = pos - self.page_len + 1;
640 } else if pos < self.offset {
641 self.offset = pos;
642 }
643 old != self.offset
644 }
645
646 #[inline]
650 pub fn scroll_to_range(&mut self, range: Range<usize>) -> bool {
651 let old = self.offset;
652 if range.start >= self.offset + self.page_len {
654 if range.end - range.start < self.page_len {
655 self.offset = range.end - self.page_len;
656 } else {
657 self.offset = range.start;
658 }
659 } else if range.start < self.offset {
660 self.offset = range.start;
661 } else if range.end >= self.offset + self.page_len {
662 if range.end - range.start < self.page_len {
663 self.offset = range.end - self.page_len;
664 } else {
665 self.offset = range.start;
666 }
667 }
668 old != self.offset
669 }
670
671 #[inline]
673 pub fn scroll_up(&mut self, n: usize) -> bool {
674 let old = self.offset;
675 self.offset = self.limited_offset(self.offset.saturating_sub(n));
676 old != self.offset
677 }
678
679 #[inline]
681 pub fn scroll_down(&mut self, n: usize) -> bool {
682 let old = self.offset;
683 self.offset = self.limited_offset(self.offset.saturating_add(n));
684 old != self.offset
685 }
686
687 #[inline]
689 pub fn scroll_left(&mut self, n: usize) -> bool {
690 self.scroll_up(n)
691 }
692
693 #[inline]
695 pub fn scroll_right(&mut self, n: usize) -> bool {
696 self.scroll_down(n)
697 }
698
699 #[inline]
701 pub fn limited_offset(&self, offset: usize) -> usize {
702 min(offset, self.max_offset.saturating_add(self.overscroll_by()))
703 }
704
705 #[inline]
710 pub fn max_offset(&self) -> usize {
711 self.max_offset
712 }
713
714 #[inline]
719 pub fn set_max_offset(&mut self, max: usize) {
720 self.max_offset = max;
721 }
722
723 #[inline]
725 pub fn page_len(&self) -> usize {
726 self.page_len
727 }
728
729 #[inline]
731 pub fn set_page_len(&mut self, page: usize) {
732 self.page_len = page;
733 }
734
735 #[inline]
738 pub fn scroll_by(&self) -> usize {
739 if let Some(scroll) = self.scroll_by {
740 max(scroll, 1)
741 } else {
742 max(self.page_len / 10, 1)
743 }
744 }
745
746 #[inline]
749 pub fn set_scroll_by(&mut self, scroll: Option<usize>) {
750 self.scroll_by = scroll;
751 }
752
753 #[inline]
755 pub fn overscroll_by(&self) -> usize {
756 self.overscroll_by.unwrap_or_default()
757 }
758
759 #[inline]
761 pub fn set_overscroll_by(&mut self, overscroll_by: Option<usize>) {
762 self.overscroll_by = overscroll_by;
763 }
764
765 #[inline]
767 pub fn items_added(&mut self, pos: usize, n: usize) {
768 if self.offset >= pos {
769 self.offset += n;
770 }
771 self.max_offset += n;
772 }
773
774 #[inline]
776 pub fn items_removed(&mut self, pos: usize, n: usize) {
777 if self.offset >= pos && self.offset >= n {
778 self.offset -= n;
779 }
780 self.max_offset = self.max_offset.saturating_sub(n);
781 }
782}
783
784impl ScrollState {
785 pub fn map_position_index(&self, pos: u16, base: u16, length: u16) -> usize {
790 let pos = pos.saturating_sub(base).saturating_sub(1) as usize;
792 let span = length.saturating_sub(2) as usize;
793
794 if span > 0 {
795 (self.max_offset.saturating_mul(pos)) / span
796 } else {
797 0
798 }
799 }
800}
801
802impl HandleEvent<crossterm::event::Event, MouseOnly, ScrollOutcome> for ScrollState {
803 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> ScrollOutcome {
804 match event {
805 ct_event!(mouse any for m) if self.mouse.drag(self.area, m) => {
806 if self.is_vertical() {
807 if m.row >= self.area.y {
808 ScrollOutcome::VPos(self.map_position_index(
809 m.row,
810 self.area.y,
811 self.area.height,
812 ))
813 } else {
814 ScrollOutcome::Unchanged
815 }
816 } else {
817 if m.column >= self.area.x {
818 ScrollOutcome::HPos(self.map_position_index(
819 m.column,
820 self.area.x,
821 self.area.width,
822 ))
823 } else {
824 ScrollOutcome::Unchanged
825 }
826 }
827 }
828 ct_event!(mouse down Left for col, row) if self.area.contains((*col, *row).into()) => {
829 if self.is_vertical() {
830 ScrollOutcome::VPos(self.map_position_index(
831 *row,
832 self.area.y,
833 self.area.height,
834 ))
835 } else {
836 ScrollOutcome::HPos(self.map_position_index(*col, self.area.x, self.area.width))
837 }
838 }
839 ct_event!(scroll down for col, row)
840 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
841 {
842 ScrollOutcome::Down(self.scroll_by())
843 }
844 ct_event!(scroll up for col, row)
845 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
846 {
847 ScrollOutcome::Up(self.scroll_by())
848 }
849 ct_event!(scroll ALT down for col, row)
851 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
852 {
853 ScrollOutcome::Right(self.scroll_by())
854 }
855 ct_event!(scroll ALT up for col, row)
857 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
858 {
859 ScrollOutcome::Left(self.scroll_by())
860 }
861 _ => ScrollOutcome::Continue,
862 }
863 }
864}