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::prelude::Style;
10#[cfg(feature = "unstable-widget-ref")]
11use ratatui::widgets::StatefulWidgetRef;
12use ratatui::widgets::{Padding, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget};
13use std::cmp::{max, min};
14use std::mem;
15use std::ops::Range;
16
17#[derive(Debug, Default, Clone)]
23pub struct Scroll<'a> {
24 policy: ScrollbarPolicy,
25 orientation: ScrollbarOrientation,
26
27 start_margin: u16,
28 end_margin: u16,
29 overscroll_by: Option<usize>,
30 scroll_by: Option<usize>,
31
32 scrollbar: Scrollbar<'a>,
33 min_style: Option<Style>,
34 min_symbol: Option<&'a str>,
35 hor_symbols: Option<ScrollSymbols>,
36 ver_symbols: Option<ScrollSymbols>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct ScrollState {
61 pub area: Rect,
64 pub orientation: ScrollbarOrientation,
67
68 pub offset: usize,
71 pub page_len: usize,
75 pub max_offset: usize,
82
83 pub scroll_by: Option<usize>,
88 pub overscroll_by: Option<usize>,
93
94 pub mouse: MouseFlags,
97
98 pub non_exhaustive: NonExhaustive,
99}
100
101#[derive(Debug, Clone)]
103pub struct ScrollStyle {
104 pub thumb_style: Option<Style>,
105 pub track_style: Option<Style>,
106 pub begin_style: Option<Style>,
107 pub end_style: Option<Style>,
108 pub min_style: Option<Style>,
109
110 pub horizontal: Option<ScrollSymbols>,
111 pub vertical: Option<ScrollSymbols>,
112
113 pub non_exhaustive: NonExhaustive,
114}
115
116#[derive(Debug, Clone, Copy)]
137pub struct ScrollSymbols {
138 pub track: &'static str,
139 pub thumb: &'static str,
140 pub begin: &'static str,
141 pub end: &'static str,
142 pub min: &'static str,
143}
144
145pub const SCROLLBAR_DOUBLE_VERTICAL: ScrollSymbols = ScrollSymbols {
146 track: ratatui::symbols::line::DOUBLE_VERTICAL,
147 thumb: ratatui::symbols::block::FULL,
148 begin: "▲",
149 end: "▼",
150 min: ratatui::symbols::line::DOUBLE_VERTICAL,
151};
152
153pub const SCROLLBAR_DOUBLE_HORIZONTAL: ScrollSymbols = ScrollSymbols {
154 track: ratatui::symbols::line::DOUBLE_HORIZONTAL,
155 thumb: ratatui::symbols::block::FULL,
156 begin: "◄",
157 end: "►",
158 min: ratatui::symbols::line::DOUBLE_HORIZONTAL,
159};
160
161pub const SCROLLBAR_VERTICAL: ScrollSymbols = ScrollSymbols {
162 track: ratatui::symbols::line::VERTICAL,
163 thumb: ratatui::symbols::block::FULL,
164 begin: "↑",
165 end: "↓",
166 min: ratatui::symbols::line::VERTICAL,
167};
168
169pub const SCROLLBAR_HORIZONTAL: ScrollSymbols = ScrollSymbols {
170 track: ratatui::symbols::line::HORIZONTAL,
171 thumb: ratatui::symbols::block::FULL,
172 begin: "←",
173 end: "→",
174 min: ratatui::symbols::line::HORIZONTAL,
175};
176
177impl From<&ScrollSymbols> for ratatui::symbols::scrollbar::Set {
178 fn from(value: &ScrollSymbols) -> Self {
179 ratatui::symbols::scrollbar::Set {
180 track: value.track,
181 thumb: value.thumb,
182 begin: value.begin,
183 end: value.end,
184 }
185 }
186}
187
188impl Default for ScrollStyle {
189 fn default() -> Self {
190 Self {
191 thumb_style: None,
192 track_style: None,
193 begin_style: None,
194 end_style: None,
195 min_style: None,
196 horizontal: None,
197 vertical: None,
198 non_exhaustive: NonExhaustive,
199 }
200 }
201}
202
203impl<'a> Scroll<'a> {
204 pub fn new() -> Self {
205 Self::default()
206 }
207
208 pub fn policy(mut self, policy: ScrollbarPolicy) -> Self {
210 self.policy = policy;
211 self
212 }
213
214 pub fn get_policy(&self) -> ScrollbarPolicy {
216 self.policy
217 }
218
219 pub fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
221 if self.orientation != orientation {
222 self.orientation = orientation.clone();
223 self.scrollbar = self.scrollbar.orientation(orientation);
224 self.update_symbols();
225 }
226 self
227 }
228
229 pub fn get_orientation(&self) -> ScrollbarOrientation {
231 self.orientation.clone()
232 }
233
234 pub fn override_vertical(mut self) -> Self {
238 let orientation = match self.orientation {
239 ScrollbarOrientation::VerticalRight => ScrollbarOrientation::VerticalRight,
240 ScrollbarOrientation::VerticalLeft => ScrollbarOrientation::VerticalLeft,
241 ScrollbarOrientation::HorizontalBottom => ScrollbarOrientation::VerticalRight,
242 ScrollbarOrientation::HorizontalTop => ScrollbarOrientation::VerticalRight,
243 };
244 if self.orientation != orientation {
245 self.orientation = orientation.clone();
246 self.scrollbar = self.scrollbar.orientation(orientation);
247 self.update_symbols();
248 }
249 self
250 }
251
252 pub fn override_horizontal(mut self) -> Self {
256 let orientation = match self.orientation {
257 ScrollbarOrientation::VerticalRight => ScrollbarOrientation::HorizontalBottom,
258 ScrollbarOrientation::VerticalLeft => ScrollbarOrientation::HorizontalBottom,
259 ScrollbarOrientation::HorizontalBottom => ScrollbarOrientation::HorizontalBottom,
260 ScrollbarOrientation::HorizontalTop => ScrollbarOrientation::HorizontalTop,
261 };
262 if self.orientation != orientation {
263 self.orientation = orientation.clone();
264 self.scrollbar = self.scrollbar.orientation(orientation);
265 self.update_symbols();
266 }
267 self
268 }
269
270 pub fn is_vertical(&self) -> bool {
272 match self.orientation {
273 ScrollbarOrientation::VerticalRight => true,
274 ScrollbarOrientation::VerticalLeft => true,
275 ScrollbarOrientation::HorizontalBottom => false,
276 ScrollbarOrientation::HorizontalTop => false,
277 }
278 }
279
280 pub fn is_horizontal(&self) -> bool {
282 match self.orientation {
283 ScrollbarOrientation::VerticalRight => false,
284 ScrollbarOrientation::VerticalLeft => false,
285 ScrollbarOrientation::HorizontalBottom => true,
286 ScrollbarOrientation::HorizontalTop => true,
287 }
288 }
289
290 pub fn start_margin(mut self, start_margin: u16) -> Self {
292 self.start_margin = start_margin;
293 self
294 }
295
296 pub fn get_start_margin(&self) -> u16 {
298 self.start_margin
299 }
300
301 pub fn end_margin(mut self, end_margin: u16) -> Self {
303 self.end_margin = end_margin;
304 self
305 }
306
307 pub fn get_end_margin(&self) -> u16 {
309 self.end_margin
310 }
311
312 pub fn overscroll_by(mut self, overscroll: usize) -> Self {
314 self.overscroll_by = Some(overscroll);
315 self
316 }
317
318 pub fn scroll_by(mut self, scroll: usize) -> Self {
320 self.scroll_by = Some(scroll);
321 self
322 }
323
324 pub fn styles(mut self, styles: ScrollStyle) -> Self {
326 if let Some(horizontal) = styles.horizontal {
327 self.hor_symbols = Some(horizontal);
328 }
329 if let Some(vertical) = styles.vertical {
330 self.ver_symbols = Some(vertical);
331 }
332 self.update_symbols();
333
334 if let Some(thumb_style) = styles.thumb_style {
335 self.scrollbar = self.scrollbar.thumb_style(thumb_style);
336 }
337 if let Some(track_style) = styles.track_style {
338 self.scrollbar = self.scrollbar.track_style(track_style);
339 }
340 if let Some(begin_style) = styles.begin_style {
341 self.scrollbar = self.scrollbar.begin_style(begin_style);
342 }
343 if let Some(end_style) = styles.end_style {
344 self.scrollbar = self.scrollbar.end_style(end_style);
345 }
346 if styles.min_style.is_some() {
347 self.min_style = styles.min_style;
348 }
349 self
350 }
351
352 fn update_symbols(&mut self) {
353 if self.is_horizontal() {
354 if let Some(horizontal) = &self.hor_symbols {
355 self.min_symbol = Some(horizontal.min);
356 self.scrollbar = mem::take(&mut self.scrollbar).symbols(horizontal.into());
357 }
358 } else {
359 if let Some(vertical) = &self.ver_symbols {
360 self.min_symbol = Some(vertical.min);
361 self.scrollbar = mem::take(&mut self.scrollbar).symbols(vertical.into());
362 }
363 }
364 }
365
366 pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
368 self.scrollbar = self.scrollbar.thumb_symbol(thumb_symbol);
369 self
370 }
371
372 pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
374 self.scrollbar = self.scrollbar.thumb_style(thumb_style);
375 self
376 }
377
378 pub fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
380 self.scrollbar = self.scrollbar.track_symbol(track_symbol);
381 self
382 }
383
384 pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
386 self.scrollbar = self.scrollbar.track_style(track_style);
387 self
388 }
389
390 pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
392 self.scrollbar = self.scrollbar.begin_symbol(begin_symbol);
393 self
394 }
395
396 pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
398 self.scrollbar = self.scrollbar.begin_style(begin_style);
399 self
400 }
401
402 pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
404 self.scrollbar = self.scrollbar.end_symbol(end_symbol);
405 self
406 }
407
408 pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
410 self.scrollbar = self.scrollbar.end_style(end_style);
411 self
412 }
413
414 pub fn min_symbol(mut self, min_symbol: Option<&'a str>) -> Self {
416 self.min_symbol = min_symbol;
417 self
418 }
419
420 pub fn min_style<S: Into<Style>>(mut self, min_style: S) -> Self {
422 self.min_style = Some(min_style.into());
423 self
424 }
425
426 pub fn symbols(mut self, symbols: &ScrollSymbols) -> Self {
428 self.min_symbol = Some(symbols.min);
429 self.scrollbar = self.scrollbar.symbols(symbols.into());
430 self
431 }
432
433 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
435 let style = style.into();
436 self.min_style = Some(style);
437 self.scrollbar = self.scrollbar.style(style);
438 self
439 }
440
441 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
474#[cfg(feature = "unstable-widget-ref")]
475impl StatefulWidgetRef for Scroll<'_> {
476 type State = ScrollState;
477
478 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
479 render_scroll(self, area, buf, state);
480 }
481}
482
483fn render_scroll(scroll: &Scroll<'_>, area: Rect, buf: &mut Buffer, state: &mut ScrollState) {
484 state.set_orientation(scroll.orientation.clone());
485 if scroll.overscroll_by.is_some() {
486 state.set_overscroll_by(scroll.overscroll_by);
487 }
488 if scroll.scroll_by.is_some() {
489 state.set_scroll_by(scroll.scroll_by);
490 }
491 state.area = area;
492
493 if area.is_empty() {
494 return;
495 }
496
497 if state.max_offset() == 0 {
498 match scroll.policy {
499 ScrollbarPolicy::Always => {
500 scroll.scrollbar().render(
501 area,
502 buf,
503 &mut ScrollbarState::new(state.max_offset())
504 .position(state.offset())
505 .viewport_content_length(state.page_len()),
506 );
507 }
508 ScrollbarPolicy::Minimize => {
509 fill(scroll.min_symbol, scroll.min_style, area, buf);
510 }
511 ScrollbarPolicy::Collapse => {
512 }
514 }
515 } else {
516 scroll.scrollbar().render(
517 area,
518 buf,
519 &mut ScrollbarState::new(state.max_offset())
520 .position(state.offset())
521 .viewport_content_length(state.page_len()),
522 );
523 }
524}
525
526fn fill(sym: Option<&'_ str>, style: Option<Style>, area: Rect, buf: &mut Buffer) {
527 let area = buf.area.intersection(area);
528 match (sym, style) {
529 (Some(sym), Some(style)) => {
530 for y in area.top()..area.bottom() {
531 for x in area.left()..area.right() {
532 if let Some(cell) = buf.cell_mut((x, y)) {
533 cell.set_symbol(sym);
535 cell.set_style(style);
536 }
537 }
538 }
539 }
540 (None, Some(style)) => {
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_style(style);
546 }
547 }
548 }
549 }
550 (Some(sym), None) => {
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_symbol(sym);
555 }
556 }
557 }
558 }
559 (None, None) => {
560 }
562 }
563}
564
565impl Default for ScrollState {
566 fn default() -> Self {
567 Self {
568 area: Default::default(),
569 orientation: Default::default(),
570 offset: 0,
571 max_offset: 0,
572 page_len: 0,
573 scroll_by: None,
574 overscroll_by: None,
575 mouse: Default::default(),
576 non_exhaustive: NonExhaustive,
577 }
578 }
579}
580
581impl RelocatableState for ScrollState {
582 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
583 self.area = relocate_area(self.area, shift, clip);
584 }
585}
586
587impl ScrollState {
588 pub fn new() -> Self {
589 Self::default()
590 }
591
592 #[inline]
593 pub fn set_orientation(&mut self, orientation: ScrollbarOrientation) {
594 self.orientation = orientation;
595 }
596
597 #[inline]
599 pub fn is_vertical(&self) -> bool {
600 self.orientation.is_vertical()
601 }
602
603 #[inline]
605 pub fn is_horizontal(&self) -> bool {
606 self.orientation.is_horizontal()
607 }
608
609 pub fn clear(&mut self) {
611 self.offset = 0;
612 }
613
614 #[inline]
616 pub fn offset(&self) -> usize {
617 self.offset
618 }
619
620 #[inline]
626 pub fn set_offset(&mut self, offset: usize) -> bool {
627 let old = self.offset;
628 self.offset = self.limit_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 + 1;
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 + 1;
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.limit_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.limit_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 limit_offset(&self, offset: usize) -> usize {
702 min(offset, self.max_offset.saturating_add(self.overscroll_by()))
703 }
704
705 #[inline]
707 pub fn clamp_offset(&self, offset: isize) -> usize {
708 offset.clamp(
709 0,
710 self.max_offset.saturating_add(self.overscroll_by()) as isize,
711 ) as usize
712 }
713
714 #[inline]
719 pub fn max_offset(&self) -> usize {
720 self.max_offset
721 }
722
723 #[inline]
728 pub fn set_max_offset(&mut self, max: usize) {
729 self.max_offset = max;
730 }
731
732 #[inline]
734 pub fn page_len(&self) -> usize {
735 self.page_len
736 }
737
738 #[inline]
740 pub fn set_page_len(&mut self, page: usize) {
741 self.page_len = page;
742 }
743
744 #[inline]
747 pub fn scroll_by(&self) -> usize {
748 if let Some(scroll) = self.scroll_by {
749 max(scroll, 1)
750 } else {
751 max(self.page_len / 10, 1)
752 }
753 }
754
755 #[inline]
758 pub fn set_scroll_by(&mut self, scroll: Option<usize>) {
759 self.scroll_by = scroll;
760 }
761
762 #[inline]
764 pub fn overscroll_by(&self) -> usize {
765 self.overscroll_by.unwrap_or_default()
766 }
767
768 #[inline]
770 pub fn set_overscroll_by(&mut self, overscroll_by: Option<usize>) {
771 self.overscroll_by = overscroll_by;
772 }
773
774 #[inline]
776 pub fn items_added(&mut self, pos: usize, n: usize) {
777 if self.offset >= pos {
778 self.offset += n;
779 }
780 self.max_offset += n;
781 }
782
783 #[inline]
785 pub fn items_removed(&mut self, pos: usize, n: usize) {
786 if self.offset >= pos && self.offset >= n {
787 self.offset -= n;
788 }
789 self.max_offset = self.max_offset.saturating_sub(n);
790 }
791}
792
793impl ScrollState {
794 pub fn map_position_index(&self, pos: u16, base: u16, length: u16) -> usize {
799 let pos = pos.saturating_sub(base).saturating_sub(1) as usize;
801 let span = length.saturating_sub(2) as usize;
802
803 if span > 0 {
804 (self.max_offset.saturating_mul(pos)) / span
805 } else {
806 0
807 }
808 }
809}
810
811impl HandleEvent<crossterm::event::Event, MouseOnly, ScrollOutcome> for ScrollState {
812 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> ScrollOutcome {
813 match event {
814 ct_event!(mouse any for m) if self.mouse.drag(self.area, m) => {
815 if self.is_vertical() {
816 if m.row >= self.area.y {
817 ScrollOutcome::VPos(self.map_position_index(
818 m.row,
819 self.area.y,
820 self.area.height,
821 ))
822 } else {
823 ScrollOutcome::Unchanged
824 }
825 } else {
826 if m.column >= self.area.x {
827 ScrollOutcome::HPos(self.map_position_index(
828 m.column,
829 self.area.x,
830 self.area.width,
831 ))
832 } else {
833 ScrollOutcome::Unchanged
834 }
835 }
836 }
837 ct_event!(mouse down Left for col, row) if self.area.contains((*col, *row).into()) => {
838 if self.is_vertical() {
839 ScrollOutcome::VPos(self.map_position_index(
840 *row,
841 self.area.y,
842 self.area.height,
843 ))
844 } else {
845 ScrollOutcome::HPos(self.map_position_index(*col, self.area.x, self.area.width))
846 }
847 }
848 ct_event!(scroll down for col, row)
849 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
850 {
851 ScrollOutcome::Down(self.scroll_by())
852 }
853 ct_event!(scroll up for col, row)
854 if self.is_vertical() && self.area.contains((*col, *row).into()) =>
855 {
856 ScrollOutcome::Up(self.scroll_by())
857 }
858 ct_event!(scroll ALT down for col, row)
860 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
861 {
862 ScrollOutcome::Right(self.scroll_by())
863 }
864 ct_event!(scroll ALT up for col, row)
866 if self.is_horizontal() && self.area.contains((*col, *row).into()) =>
867 {
868 ScrollOutcome::Left(self.scroll_by())
869 }
870 _ => ScrollOutcome::Continue,
871 }
872 }
873}