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::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 policy: Option<ScrollbarPolicy>,
113
114 pub non_exhaustive: NonExhaustive,
115}
116
117#[derive(Debug, Clone, Copy)]
138pub struct ScrollSymbols {
139 pub track: &'static str,
140 pub thumb: &'static str,
141 pub begin: &'static str,
142 pub end: &'static str,
143 pub min: &'static str,
144}
145
146pub const SCROLLBAR_DOUBLE_VERTICAL: ScrollSymbols = ScrollSymbols {
147 track: symbols::line::DOUBLE_VERTICAL,
148 thumb: symbols::block::FULL,
149 begin: "▲",
150 end: "▼",
151 min: symbols::line::DOUBLE_VERTICAL,
152};
153
154pub const SCROLLBAR_DOUBLE_HORIZONTAL: ScrollSymbols = ScrollSymbols {
155 track: symbols::line::DOUBLE_HORIZONTAL,
156 thumb: symbols::block::FULL,
157 begin: "◄",
158 end: "►",
159 min: symbols::line::DOUBLE_HORIZONTAL,
160};
161
162pub const SCROLLBAR_VERTICAL: ScrollSymbols = ScrollSymbols {
163 track: symbols::line::VERTICAL,
164 thumb: symbols::block::FULL,
165 begin: "↑",
166 end: "↓",
167 min: symbols::line::VERTICAL,
168};
169
170pub const SCROLLBAR_HORIZONTAL: ScrollSymbols = ScrollSymbols {
171 track: symbols::line::HORIZONTAL,
172 thumb: symbols::block::FULL,
173 begin: "←",
174 end: "→",
175 min: symbols::line::HORIZONTAL,
176};
177
178impl From<&ScrollSymbols> for symbols::scrollbar::Set {
179 fn from(value: &ScrollSymbols) -> Self {
180 symbols::scrollbar::Set {
181 track: value.track,
182 thumb: value.thumb,
183 begin: value.begin,
184 end: value.end,
185 }
186 }
187}
188
189impl Default for ScrollStyle {
190 fn default() -> Self {
191 Self {
192 thumb_style: None,
193 track_style: None,
194 begin_style: None,
195 end_style: None,
196 min_style: None,
197 horizontal: None,
198 vertical: None,
199 policy: None,
200 non_exhaustive: NonExhaustive,
201 }
202 }
203}
204
205impl<'a> Scroll<'a> {
206 pub fn new() -> Self {
207 Self::default()
208 }
209
210 pub fn horizontal() -> Self {
212 Self::default().orientation(ScrollbarOrientation::HorizontalBottom)
213 }
214
215 pub fn vertical() -> Self {
217 Self::default().orientation(ScrollbarOrientation::VerticalRight)
218 }
219
220 pub fn policy(mut self, policy: ScrollbarPolicy) -> Self {
222 self.policy = policy;
223 self
224 }
225
226 pub fn get_policy(&self) -> ScrollbarPolicy {
228 self.policy
229 }
230
231 pub fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
233 if self.orientation != orientation {
234 self.orientation = orientation.clone();
235 self.scrollbar = self.scrollbar.orientation(orientation);
236 self.update_symbols();
237 }
238 self
239 }
240
241 pub fn get_orientation(&self) -> ScrollbarOrientation {
243 self.orientation.clone()
244 }
245
246 pub fn override_vertical(mut self) -> Self {
250 let orientation = match self.orientation {
251 ScrollbarOrientation::VerticalRight => ScrollbarOrientation::VerticalRight,
252 ScrollbarOrientation::VerticalLeft => ScrollbarOrientation::VerticalLeft,
253 ScrollbarOrientation::HorizontalBottom => ScrollbarOrientation::VerticalRight,
254 ScrollbarOrientation::HorizontalTop => ScrollbarOrientation::VerticalRight,
255 };
256 if self.orientation != orientation {
257 self.orientation = orientation.clone();
258 self.scrollbar = self.scrollbar.orientation(orientation);
259 self.update_symbols();
260 }
261 self
262 }
263
264 pub fn override_horizontal(mut self) -> Self {
268 let orientation = match self.orientation {
269 ScrollbarOrientation::VerticalRight => ScrollbarOrientation::HorizontalBottom,
270 ScrollbarOrientation::VerticalLeft => ScrollbarOrientation::HorizontalBottom,
271 ScrollbarOrientation::HorizontalBottom => ScrollbarOrientation::HorizontalBottom,
272 ScrollbarOrientation::HorizontalTop => ScrollbarOrientation::HorizontalTop,
273 };
274 if self.orientation != orientation {
275 self.orientation = orientation.clone();
276 self.scrollbar = self.scrollbar.orientation(orientation);
277 self.update_symbols();
278 }
279 self
280 }
281
282 pub fn is_vertical(&self) -> bool {
284 match self.orientation {
285 ScrollbarOrientation::VerticalRight => true,
286 ScrollbarOrientation::VerticalLeft => true,
287 ScrollbarOrientation::HorizontalBottom => false,
288 ScrollbarOrientation::HorizontalTop => false,
289 }
290 }
291
292 pub fn is_horizontal(&self) -> bool {
294 match self.orientation {
295 ScrollbarOrientation::VerticalRight => false,
296 ScrollbarOrientation::VerticalLeft => false,
297 ScrollbarOrientation::HorizontalBottom => true,
298 ScrollbarOrientation::HorizontalTop => true,
299 }
300 }
301
302 pub fn start_margin(mut self, start_margin: u16) -> Self {
304 self.start_margin = start_margin;
305 self
306 }
307
308 pub fn get_start_margin(&self) -> u16 {
310 self.start_margin
311 }
312
313 pub fn end_margin(mut self, end_margin: u16) -> Self {
315 self.end_margin = end_margin;
316 self
317 }
318
319 pub fn get_end_margin(&self) -> u16 {
321 self.end_margin
322 }
323
324 pub fn overscroll_by(mut self, overscroll: usize) -> Self {
326 self.overscroll_by = Some(overscroll);
327 self
328 }
329
330 pub fn scroll_by(mut self, scroll: usize) -> Self {
332 self.scroll_by = Some(scroll);
333 self
334 }
335
336 pub fn styles(mut self, styles: ScrollStyle) -> Self {
338 if let Some(horizontal) = styles.horizontal {
339 self.hor_symbols = Some(horizontal);
340 }
341 if let Some(vertical) = styles.vertical {
342 self.ver_symbols = Some(vertical);
343 }
344 self.update_symbols();
345
346 if let Some(thumb_style) = styles.thumb_style {
347 self.scrollbar = self.scrollbar.thumb_style(thumb_style);
348 }
349 if let Some(track_style) = styles.track_style {
350 self.scrollbar = self.scrollbar.track_style(track_style);
351 }
352 if let Some(begin_style) = styles.begin_style {
353 self.scrollbar = self.scrollbar.begin_style(begin_style);
354 }
355 if let Some(end_style) = styles.end_style {
356 self.scrollbar = self.scrollbar.end_style(end_style);
357 }
358 if styles.min_style.is_some() {
359 self.min_style = styles.min_style;
360 }
361 if let Some(policy) = styles.policy {
362 self.policy = policy;
363 }
364 self
365 }
366
367 fn update_symbols(&mut self) {
368 if self.is_horizontal() {
369 if let Some(horizontal) = &self.hor_symbols {
370 self.min_symbol = Some(horizontal.min);
371 self.scrollbar = mem::take(&mut self.scrollbar).symbols(horizontal.into());
372 }
373 } else {
374 if let Some(vertical) = &self.ver_symbols {
375 self.min_symbol = Some(vertical.min);
376 self.scrollbar = mem::take(&mut self.scrollbar).symbols(vertical.into());
377 }
378 }
379 }
380
381 pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
383 self.scrollbar = self.scrollbar.thumb_symbol(thumb_symbol);
384 self
385 }
386
387 pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
389 self.scrollbar = self.scrollbar.thumb_style(thumb_style);
390 self
391 }
392
393 pub fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
395 self.scrollbar = self.scrollbar.track_symbol(track_symbol);
396 self
397 }
398
399 pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
401 self.scrollbar = self.scrollbar.track_style(track_style);
402 self
403 }
404
405 pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
407 self.scrollbar = self.scrollbar.begin_symbol(begin_symbol);
408 self
409 }
410
411 pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
413 self.scrollbar = self.scrollbar.begin_style(begin_style);
414 self
415 }
416
417 pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
419 self.scrollbar = self.scrollbar.end_symbol(end_symbol);
420 self
421 }
422
423 pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
425 self.scrollbar = self.scrollbar.end_style(end_style);
426 self
427 }
428
429 pub fn min_symbol(mut self, min_symbol: Option<&'a str>) -> Self {
431 self.min_symbol = min_symbol;
432 self
433 }
434
435 pub fn min_style<S: Into<Style>>(mut self, min_style: S) -> Self {
437 self.min_style = Some(min_style.into());
438 self
439 }
440
441 pub fn symbols(mut self, symbols: &ScrollSymbols) -> Self {
443 self.min_symbol = Some(symbols.min);
444 self.scrollbar = self.scrollbar.symbols(symbols.into());
445 self
446 }
447
448 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
450 let style = style.into();
451 self.min_style = Some(style);
452 self.scrollbar = self.scrollbar.style(style);
453 self
454 }
455
456 pub fn padding(&self) -> Padding {
458 match self.orientation {
459 ScrollbarOrientation::VerticalRight => Padding::new(0, 1, 0, 0),
460 ScrollbarOrientation::VerticalLeft => Padding::new(1, 0, 0, 0),
461 ScrollbarOrientation::HorizontalBottom => Padding::new(0, 0, 0, 1),
462 ScrollbarOrientation::HorizontalTop => Padding::new(0, 0, 1, 0),
463 }
464 }
465}
466
467impl<'a> Scroll<'a> {
468 fn scrollbar(&self) -> Scrollbar<'a> {
470 self.scrollbar.clone()
471 }
472}
473
474impl StatefulWidget for &Scroll<'_> {
475 type State = ScrollState;
476
477 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
478 render_scroll(self, area, buf, state);
479 }
480}
481
482impl StatefulWidget for Scroll<'_> {
483 type State = ScrollState;
484
485 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
486 render_scroll(&self, area, buf, state);
487 }
488}
489
490fn render_scroll(scroll: &Scroll<'_>, area: Rect, buf: &mut Buffer, state: &mut ScrollState) {
491 state.set_orientation(scroll.orientation.clone());
492 if scroll.overscroll_by.is_some() {
493 state.set_overscroll_by(scroll.overscroll_by);
494 }
495 if scroll.scroll_by.is_some() {
496 state.set_scroll_by(scroll.scroll_by);
497 }
498 state.area = area;
499
500 if area.is_empty() {
501 return;
502 }
503
504 if state.max_offset() == 0 {
505 match scroll.policy {
506 ScrollbarPolicy::Always => {
507 scroll.scrollbar().render(
508 area,
509 buf,
510 &mut ScrollbarState::new(1)
511 .position(state.offset())
512 .viewport_content_length(state.page_len()),
513 );
514 }
515 ScrollbarPolicy::Minimize => {
516 fill(scroll.min_symbol, scroll.min_style, area, buf);
517 }
518 ScrollbarPolicy::Collapse => {
519 }
521 }
522 } else {
523 scroll.scrollbar().render(
524 area,
525 buf,
526 &mut ScrollbarState::new(state.max_offset())
527 .position(state.offset())
528 .viewport_content_length(state.page_len()),
529 );
530 }
531}
532
533fn fill(sym: Option<&'_ str>, style: Option<Style>, area: Rect, buf: &mut Buffer) {
534 let area = buf.area.intersection(area);
535 match (sym, style) {
536 (Some(sym), Some(style)) => {
537 for y in area.top()..area.bottom() {
538 for x in area.left()..area.right() {
539 if let Some(cell) = buf.cell_mut((x, y)) {
540 cell.set_symbol(sym);
542 cell.set_style(style);
543 }
544 }
545 }
546 }
547 (None, Some(style)) => {
548 for y in area.top()..area.bottom() {
549 for x in area.left()..area.right() {
550 if let Some(cell) = buf.cell_mut((x, y)) {
551 cell.set_style(style);
553 }
554 }
555 }
556 }
557 (Some(sym), None) => {
558 for y in area.top()..area.bottom() {
559 for x in area.left()..area.right() {
560 if let Some(cell) = buf.cell_mut((x, y)) {
561 cell.set_symbol(sym);
562 }
563 }
564 }
565 }
566 (None, None) => {
567 }
569 }
570}
571
572impl Default for ScrollState {
573 fn default() -> Self {
574 Self {
575 area: Default::default(),
576 orientation: Default::default(),
577 offset: 0,
578 max_offset: 0,
579 page_len: 0,
580 scroll_by: None,
581 overscroll_by: None,
582 mouse: Default::default(),
583 non_exhaustive: NonExhaustive,
584 }
585 }
586}
587
588impl RelocatableState for ScrollState {
589 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
590 self.area.relocate(shift, clip);
591 }
592}
593
594impl ScrollState {
595 pub fn new() -> Self {
596 Self::default()
597 }
598
599 #[inline]
600 pub fn set_orientation(&mut self, orientation: ScrollbarOrientation) {
601 self.orientation = orientation;
602 }
603
604 #[inline]
606 pub fn is_vertical(&self) -> bool {
607 self.orientation.is_vertical()
608 }
609
610 #[inline]
612 pub fn is_horizontal(&self) -> bool {
613 self.orientation.is_horizontal()
614 }
615
616 pub fn clear(&mut self) {
618 self.offset = 0;
619 }
620
621 #[inline]
623 pub fn offset(&self) -> usize {
624 self.offset
625 }
626
627 #[inline]
632 pub fn set_offset(&mut self, offset: usize) -> bool {
633 let old = self.offset;
634 self.offset = offset;
635 old != self.offset
636 }
637
638 #[inline]
642 pub fn scroll_to_pos(&mut self, pos: usize) -> bool {
643 let old = self.offset;
644 if pos >= self.offset + self.page_len {
645 self.offset = pos - self.page_len + 1;
646 } else if pos < self.offset {
647 self.offset = pos;
648 }
649 old != self.offset
650 }
651
652 #[inline]
656 pub fn scroll_to_range(&mut self, range: Range<usize>) -> bool {
657 let old = self.offset;
658 if range.start >= self.offset + self.page_len {
660 if range.end - range.start < self.page_len {
661 self.offset = range.end - self.page_len;
662 } else {
663 self.offset = range.start;
664 }
665 } else if range.start < self.offset {
666 self.offset = range.start;
667 } else if range.end >= self.offset + self.page_len {
668 if range.end - range.start < self.page_len {
669 self.offset = range.end - self.page_len;
670 } else {
671 self.offset = range.start;
672 }
673 }
674 old != self.offset
675 }
676
677 #[inline]
679 pub fn scroll_up(&mut self, n: usize) -> bool {
680 let old = self.offset;
681 self.offset = self.limited_offset(self.offset.saturating_sub(n));
682 old != self.offset
683 }
684
685 #[inline]
687 pub fn scroll_down(&mut self, n: usize) -> bool {
688 let old = self.offset;
689 self.offset = self.limited_offset(self.offset.saturating_add(n));
690 old != self.offset
691 }
692
693 #[inline]
695 pub fn scroll_left(&mut self, n: usize) -> bool {
696 self.scroll_up(n)
697 }
698
699 #[inline]
701 pub fn scroll_right(&mut self, n: usize) -> bool {
702 self.scroll_down(n)
703 }
704
705 #[inline]
707 pub fn limited_offset(&self, offset: usize) -> usize {
708 min(offset, self.max_offset.saturating_add(self.overscroll_by()))
709 }
710
711 #[inline]
716 pub fn max_offset(&self) -> usize {
717 self.max_offset
718 }
719
720 #[inline]
725 pub fn set_max_offset(&mut self, max: usize) {
726 self.max_offset = max;
727 }
728
729 #[inline]
731 pub fn page_len(&self) -> usize {
732 self.page_len
733 }
734
735 #[inline]
737 pub fn set_page_len(&mut self, page: usize) {
738 self.page_len = page;
739 }
740
741 #[inline]
744 pub fn scroll_by(&self) -> usize {
745 if let Some(scroll) = self.scroll_by {
746 max(scroll, 1)
747 } else {
748 max(self.page_len / 10, 1)
749 }
750 }
751
752 #[inline]
755 pub fn set_scroll_by(&mut self, scroll: Option<usize>) {
756 self.scroll_by = scroll;
757 }
758
759 #[inline]
761 pub fn overscroll_by(&self) -> usize {
762 self.overscroll_by.unwrap_or_default()
763 }
764
765 #[inline]
767 pub fn set_overscroll_by(&mut self, overscroll_by: Option<usize>) {
768 self.overscroll_by = overscroll_by;
769 }
770
771 #[inline]
773 pub fn items_added(&mut self, pos: usize, n: usize) {
774 if self.offset >= pos {
775 self.offset += n;
776 }
777 self.max_offset += n;
778 }
779
780 #[inline]
782 pub fn items_removed(&mut self, pos: usize, n: usize) {
783 if self.offset >= pos && self.offset >= n {
784 self.offset -= n;
785 }
786 self.max_offset = self.max_offset.saturating_sub(n);
787 }
788}
789
790impl ScrollState {
791 pub fn map_position_index(&self, pos: u16, base: u16, length: u16) -> usize {
796 let pos = pos.saturating_sub(base).saturating_sub(1) as usize;
798 let span = length.saturating_sub(2) as usize;
799
800 if span > 0 {
801 (self.max_offset.saturating_mul(pos)) / span
802 } else {
803 0
804 }
805 }
806}
807
808impl HandleEvent<crossterm::event::Event, MouseOnly, ScrollOutcome> for ScrollState {
809 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> ScrollOutcome {
810 match event {
811 ct_event!(mouse any for m) if self.mouse.drag(self.area, m) => {
812 if self.is_vertical() {
813 if m.row >= self.area.y {
814 ScrollOutcome::VPos(self.map_position_index(
815 m.row,
816 self.area.y,
817 self.area.height,
818 ))
819 } else {
820 ScrollOutcome::Unchanged
821 }
822 } else {
823 if m.column >= self.area.x {
824 ScrollOutcome::HPos(self.map_position_index(
825 m.column,
826 self.area.x,
827 self.area.width,
828 ))
829 } else {
830 ScrollOutcome::Unchanged
831 }
832 }
833 }
834 ct_event!(mouse down Left for col, row) if self.area.contains((*col, *row).into()) => {
835 if self.is_vertical() {
836 ScrollOutcome::VPos(self.map_position_index(
837 *row,
838 self.area.y,
839 self.area.height,
840 ))
841 } else {
842 ScrollOutcome::HPos(self.map_position_index(*col, self.area.x, self.area.width))
843 }
844 }
845 ct_event!(scroll down for col, row)
846 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
847 {
848 ScrollOutcome::Down(self.scroll_by())
849 }
850 ct_event!(scroll up for col, row)
851 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
852 {
853 ScrollOutcome::Up(self.scroll_by())
854 }
855 ct_event!(scroll ALT down for col, row)
857 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
858 {
859 ScrollOutcome::Right(self.scroll_by())
860 }
861 ct_event!(scroll ALT up for col, row)
863 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
864 {
865 ScrollOutcome::Left(self.scroll_by())
866 }
867 _ => ScrollOutcome::Continue,
868 }
869 }
870}