scrin 0.1.83

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::core::buffer::Buffer;
use crate::core::rect::Rect;
use crate::interaction::InteractionLayer;
use crate::scroll_state::{ScrollState, StickyScroll};
use crate::widgets::markdown_output::{OutputTheme, RetainedMarkdownOutput};
use crate::widgets::Widget;

#[derive(Debug, Clone)]
pub struct TranscriptViewport {
    pub output: RetainedMarkdownOutput,
    pub viewport: Rect,
    pub scroll: ScrollState,
    pub bottom_relative: bool,
}

impl TranscriptViewport {
    pub fn new(content: &str) -> Self {
        Self {
            output: RetainedMarkdownOutput::new(content),
            viewport: Rect::ZERO,
            scroll: ScrollState::new(),
            bottom_relative: false,
        }
    }

    pub fn with_theme(mut self, theme: OutputTheme) -> Self {
        self.output = self.output.with_theme(theme);
        self
    }

    pub fn with_code_line_numbers(mut self, show: bool) -> Self {
        self.output = self.output.with_code_line_numbers(show);
        self
    }

    pub fn with_selectable(mut self, selectable: bool) -> Self {
        self.output = self.output.with_selectable(selectable);
        self
    }

    pub fn with_region_id(mut self, id: impl Into<crate::interaction::WidgetId>) -> Self {
        self.output = self.output.with_region_id(id);
        self
    }

    pub fn with_viewport(mut self, viewport: Rect) -> Self {
        self.viewport = viewport;
        self.sync_bounds();
        self
    }

    pub fn with_scroll(mut self, scroll: ScrollState) -> Self {
        self.scroll = scroll;
        self.sync_bounds();
        self
    }

    pub fn with_bottom_relative(mut self, bottom_relative: bool) -> Self {
        self.bottom_relative = bottom_relative;
        if bottom_relative {
            self.scroll.sticky = StickyScroll::Bottom;
        }
        self.sync_bounds();
        self
    }

    pub fn set_content(&mut self, content: &str) {
        self.output.set_content(content);
        self.sync_bounds();
    }

    pub fn row_count_for_width(&self, width: u16) -> usize {
        self.output
            .with_rows_for_width(width as usize, |rows| rows.len())
    }

    pub fn max_offset(&mut self) -> usize {
        self.sync_bounds();
        self.scroll.max_offset()
    }

    pub fn scroll_up(&mut self, lines: usize) {
        self.sync_bounds();
        self.scroll.scroll_up(lines);
    }

    pub fn scroll_down(&mut self, lines: usize) {
        self.sync_bounds();
        self.scroll.scroll_down(lines);
    }

    pub fn scroll_to_top(&mut self) {
        self.sync_bounds();
        self.scroll.scroll_to_top();
    }

    pub fn scroll_to_bottom(&mut self) {
        self.sync_bounds();
        self.scroll.scroll_to_bottom();
    }

    pub fn status_text(&mut self) -> String {
        self.sync_bounds();
        if self.scroll.is_at_bottom() {
            "bottom".to_string()
        } else {
            format!(
                "up {}/{}",
                self.scroll.max_offset() - self.scroll.offset,
                self.scroll.total
            )
        }
    }

    pub fn render(&mut self, buffer: &mut Buffer) {
        self.render_in(buffer, self.viewport);
    }

    pub fn render_in(&mut self, buffer: &mut Buffer, area: Rect) {
        self.viewport = area;
        self.sync_bounds();
        self.output.output.scroll = self.scroll;
        self.output.render(buffer, area);
    }

    pub fn render_with_interaction(
        &mut self,
        buffer: &mut Buffer,
        area: Rect,
        layer: &mut InteractionLayer,
    ) {
        self.viewport = area;
        self.sync_bounds();
        self.output.output.scroll = self.scroll;
        self.output.render_with_interaction(buffer, area, layer);
    }

    fn sync_bounds(&mut self) {
        let total = self.row_count_for_width(self.viewport.width.max(1));
        self.scroll.set_bounds(total, self.viewport.height as usize);
        if self.bottom_relative {
            self.scroll.sticky = StickyScroll::Bottom;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn transcript_viewport_uses_same_rows_for_bounds_and_render() {
        let mut viewport = TranscriptViewport::new("one two three four")
            .with_viewport(Rect::new(0, 0, 8, 2))
            .with_selectable(true)
            .with_region_id("transcript");
        assert!(viewport.row_count_for_width(8) >= 2);
        assert_eq!(viewport.max_offset(), viewport.scroll.max_offset());

        let mut buffer = Buffer::new(8, 2);
        let mut layer = InteractionLayer::new();
        viewport.render_with_interaction(&mut buffer, Rect::new(0, 0, 8, 2), &mut layer);
        assert_eq!(
            layer.scroll_hit_test(0, 0).unwrap().region_id.as_ref(),
            "transcript"
        );
    }
}