use crate::core::geometry::Rect;
use crate::core::event::{Event, EventType, KB_UP, KB_DOWN, KB_PGUP, KB_PGDN, KB_HOME, KB_END};
use crate::core::draw::DrawBuffer;
use crate::core::palette::Attr;
use crate::core::state::StateFlags;
use crate::terminal::Terminal;
use super::view::{View, write_line_to_terminal};
use super::scrollbar::ScrollBar;
#[derive(Clone, Debug)]
pub struct OutputLine {
pub text: String,
pub attr: Option<Attr>,
}
impl OutputLine {
pub fn new(text: String) -> Self {
Self { text, attr: None }
}
pub fn with_attr(text: String, attr: Attr) -> Self {
Self { text, attr: Some(attr) }
}
}
pub struct TerminalWidget {
bounds: Rect,
state: StateFlags,
lines: Vec<OutputLine>,
max_lines: usize,
top_line: usize,
auto_scroll: bool,
v_scrollbar: Option<Box<ScrollBar>>,
palette_chain: Option<crate::core::palette_chain::PaletteChainNode>,
}
impl TerminalWidget {
pub fn new(bounds: Rect) -> Self {
Self {
bounds,
state: 0,
lines: Vec::new(),
max_lines: 10000, top_line: 0,
auto_scroll: true,
v_scrollbar: None,
palette_chain: None,
}
}
pub fn with_scrollbar(mut self) -> Self {
let v_bounds = Rect::new(
self.bounds.b.x - 1,
self.bounds.a.y,
self.bounds.b.x,
self.bounds.b.y,
);
self.v_scrollbar = Some(Box::new(ScrollBar::new_vertical(v_bounds)));
self
}
pub fn set_max_lines(&mut self, max_lines: usize) {
self.max_lines = max_lines;
self.trim_buffer();
}
pub fn set_auto_scroll(&mut self, auto_scroll: bool) {
self.auto_scroll = auto_scroll;
}
pub fn append_line(&mut self, text: String) {
self.lines.push(OutputLine::new(text));
self.trim_buffer();
if self.auto_scroll {
self.scroll_to_bottom();
}
self.update_scrollbar();
}
pub fn append_line_colored(&mut self, text: String, attr: Attr) {
self.lines.push(OutputLine::with_attr(text, attr));
self.trim_buffer();
if self.auto_scroll {
self.scroll_to_bottom();
}
self.update_scrollbar();
}
pub fn append_lines(&mut self, lines: Vec<String>) {
for line in lines {
self.lines.push(OutputLine::new(line));
}
self.trim_buffer();
if self.auto_scroll {
self.scroll_to_bottom();
}
self.update_scrollbar();
}
pub fn append_text(&mut self, text: &str) {
for line in text.lines() {
self.lines.push(OutputLine::new(line.to_string()));
}
self.trim_buffer();
if self.auto_scroll {
self.scroll_to_bottom();
}
self.update_scrollbar();
}
pub fn clear(&mut self) {
self.lines.clear();
self.top_line = 0;
self.update_scrollbar();
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn scroll_to_bottom(&mut self) {
let visible_rows = self.get_visible_rows();
if self.lines.len() > visible_rows {
self.top_line = self.lines.len() - visible_rows;
} else {
self.top_line = 0;
}
}
pub fn scroll_to_top(&mut self) {
self.top_line = 0;
}
fn trim_buffer(&mut self) {
if self.lines.len() > self.max_lines {
let excess = self.lines.len() - self.max_lines;
self.lines.drain(0..excess);
if self.top_line >= excess {
self.top_line -= excess;
} else {
self.top_line = 0;
}
}
}
fn get_visible_rows(&self) -> usize {
let mut height = self.bounds.height_clamped() as usize;
if self.v_scrollbar.is_some() {
height = height.saturating_sub(0); }
height
}
fn get_visible_width(&self) -> usize {
let mut width = self.bounds.width_clamped() as usize;
if self.v_scrollbar.is_some() {
width = width.saturating_sub(1); }
width
}
fn update_scrollbar(&mut self) {
let visible_rows = self.get_visible_rows();
let total_lines = self.lines.len();
let top_line = self.top_line;
let max_scroll = if total_lines > visible_rows {
total_lines - visible_rows
} else {
0
};
if let Some(ref mut v_bar) = self.v_scrollbar {
v_bar.set_params(
top_line as i32,
0,
max_scroll as i32,
visible_rows as i32,
1,
);
}
}
fn scroll_up(&mut self) {
if self.top_line > 0 {
self.top_line -= 1;
self.auto_scroll = false; self.update_scrollbar();
}
}
fn scroll_down(&mut self) {
let visible_rows = self.get_visible_rows();
if self.top_line + visible_rows < self.lines.len() {
self.top_line += 1;
self.update_scrollbar();
if self.top_line + visible_rows >= self.lines.len() {
self.auto_scroll = true;
}
}
}
fn page_up(&mut self) {
let visible_rows = self.get_visible_rows();
self.top_line = self.top_line.saturating_sub(visible_rows);
self.auto_scroll = false; self.update_scrollbar();
}
fn page_down(&mut self) {
let visible_rows = self.get_visible_rows();
let max_scroll = if self.lines.len() > visible_rows {
self.lines.len() - visible_rows
} else {
0
};
self.top_line = (self.top_line + visible_rows).min(max_scroll);
self.update_scrollbar();
if self.top_line + visible_rows >= self.lines.len() {
self.auto_scroll = true;
}
}
}
impl View for TerminalWidget {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
if self.v_scrollbar.is_some() {
let v_bounds = Rect::new(
bounds.b.x - 1,
bounds.a.y,
bounds.b.x,
bounds.b.y,
);
self.v_scrollbar = Some(Box::new(ScrollBar::new_vertical(v_bounds)));
}
self.update_scrollbar();
}
fn draw(&mut self, terminal: &mut Terminal) {
let visible_rows = self.get_visible_rows();
let visible_width = self.get_visible_width();
let default_color = Attr::new(crate::core::palette::TvColor::LightGray, crate::core::palette::TvColor::Black);
for i in 0..visible_rows {
let line_idx = self.top_line + i;
let mut buf = DrawBuffer::new(visible_width);
if line_idx < self.lines.len() {
let line = &self.lines[line_idx];
let color = line.attr.unwrap_or(default_color);
let text = if line.text.len() > visible_width {
&line.text[..visible_width]
} else {
&line.text
};
buf.move_str(0, text, color);
if text.len() < visible_width {
buf.move_char(text.len(), ' ', color, visible_width - text.len());
}
} else {
buf.move_char(0, ' ', default_color, visible_width);
}
write_line_to_terminal(terminal, self.bounds.a.x, self.bounds.a.y + i as i16, &buf);
}
if let Some(ref mut v_bar) = self.v_scrollbar {
v_bar.draw(terminal);
}
}
fn handle_event(&mut self, event: &mut Event) {
match event.what {
EventType::Keyboard => {
match event.key_code {
KB_UP => {
self.scroll_up();
event.clear();
}
KB_DOWN => {
self.scroll_down();
event.clear();
}
KB_PGUP => {
self.page_up();
event.clear();
}
KB_PGDN => {
self.page_down();
event.clear();
}
KB_HOME => {
self.scroll_to_top();
self.auto_scroll = false;
self.update_scrollbar();
event.clear();
}
KB_END => {
self.scroll_to_bottom();
self.auto_scroll = true;
self.update_scrollbar();
event.clear();
}
_ => {}
}
}
EventType::MouseWheelUp => {
if self.bounds.contains(event.mouse.pos) {
self.scroll_up();
event.clear();
}
}
EventType::MouseWheelDown => {
if self.bounds.contains(event.mouse.pos) {
self.scroll_down();
event.clear();
}
}
_ => {}
}
}
fn state(&self) -> StateFlags {
self.state
}
fn set_state(&mut self, state: StateFlags) {
self.state = state;
}
fn can_focus(&self) -> bool {
true
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn get_palette(&self) -> Option<crate::core::palette::Palette> {
use crate::core::palette::{palettes, Palette};
Some(Palette::from_slice(palettes::CP_SCROLLER))
}
fn set_palette_chain(&mut self, node: Option<crate::core::palette_chain::PaletteChainNode>) {
self.palette_chain = node;
}
fn get_palette_chain(&self) -> Option<&crate::core::palette_chain::PaletteChainNode> {
self.palette_chain.as_ref()
}
}
pub struct TerminalWidgetBuilder {
bounds: Option<Rect>,
with_scrollbar: bool,
max_lines: usize,
auto_scroll: bool,
}
impl TerminalWidgetBuilder {
pub fn new() -> Self {
Self {
bounds: None,
with_scrollbar: false,
max_lines: 10000,
auto_scroll: true,
}
}
#[must_use]
pub fn bounds(mut self, bounds: Rect) -> Self {
self.bounds = Some(bounds);
self
}
#[must_use]
pub fn with_scrollbar(mut self, with_scrollbar: bool) -> Self {
self.with_scrollbar = with_scrollbar;
self
}
#[must_use]
pub fn max_lines(mut self, max_lines: usize) -> Self {
self.max_lines = max_lines;
self
}
#[must_use]
pub fn auto_scroll(mut self, auto_scroll: bool) -> Self {
self.auto_scroll = auto_scroll;
self
}
pub fn build(self) -> TerminalWidget {
let bounds = self.bounds.expect("TerminalWidget bounds must be set");
let mut widget = TerminalWidget::new(bounds);
if self.with_scrollbar {
widget = widget.with_scrollbar();
}
widget.set_max_lines(self.max_lines);
widget.set_auto_scroll(self.auto_scroll);
widget
}
pub fn build_boxed(self) -> Box<TerminalWidget> {
Box::new(self.build())
}
}
impl Default for TerminalWidgetBuilder {
fn default() -> Self {
Self::new()
}
}