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"
);
}
}