use crate::input::scroll::Scroller;
use crate::widgets::common::WindowView;
use crate::widgets::text::{Alignment, TextBlock};
use crate::widgets::{BorderChars, Widget};
use crate::{Color, ColorPair, Result, Window};
pub struct Panel {
width: u16,
height: u16,
header_text: String,
body_content: PanelContent,
header_style: BorderChars,
body_style: BorderChars,
header_color: Option<ColorPair>,
body_color: Option<ColorPair>,
header_border_color: Option<ColorPair>,
body_border_color: Option<ColorPair>,
padding: u16,
alignment: Alignment, auto_size: bool,
scroller: Scroller,
scrollable: bool,
show_scroll_indicators: bool,
}
pub(crate) enum PanelContent {
Text(String),
Block(Box<TextBlock>),
}
impl Panel {
pub fn new(width: u16, height: u16) -> Self {
Self {
width,
height,
header_text: "".to_string(),
body_content: PanelContent::Text(String::new()),
header_style: BorderChars::single_line(),
body_style: BorderChars::single_line(),
header_color: None,
body_color: None,
header_border_color: None,
body_border_color: None,
padding: 1,
alignment: Alignment::Left,
auto_size: width == 0 || height == 0, scroller: Scroller::new(),
scrollable: false,
show_scroll_indicators: false,
}
}
pub fn auto_sized() -> Self {
Self::new(0, 0)
}
pub fn with_header(mut self, text: impl Into<String>) -> Self {
self.header_text = text.into();
if self.auto_size {
self.adjust_size();
}
self
}
pub fn with_body(mut self, text: impl Into<String>) -> Self {
self.body_content = PanelContent::Text(text.into());
if self.auto_size {
self.adjust_size();
}
self
}
pub fn with_body_block(mut self, text_block: TextBlock) -> Self {
self.body_content = PanelContent::Block(Box::new(text_block));
if self.auto_size {
self.adjust_size();
}
self
}
pub fn with_header_style(mut self, style: BorderChars) -> Self {
self.header_style = style;
self
}
pub fn with_body_style(mut self, style: BorderChars) -> Self {
self.body_style = style;
self
}
pub fn with_header_color(mut self, color: Option<ColorPair>) -> Self {
self.header_color = color;
self
}
pub fn with_body_color(mut self, color: Option<ColorPair>) -> Self {
self.body_color = color;
self
}
pub fn with_header_border_color(mut self, color: Color) -> Self {
self.header_border_color = Some(ColorPair::new(color, Color::Transparent));
self
}
pub fn with_body_border_color(mut self, color: Color) -> Self {
self.body_border_color = Some(ColorPair::new(color, Color::Transparent));
self
}
pub fn with_padding(mut self, padding: u16) -> Self {
self.padding = padding;
if self.auto_size {
self.adjust_size();
}
self
}
pub fn with_alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn set_header(&mut self, text: impl Into<String>) {
self.header_text = text.into();
if self.auto_size {
self.adjust_size();
}
}
pub fn set_body(&mut self, text: impl Into<String>) {
self.body_content = PanelContent::Text(text.into());
if self.auto_size {
self.adjust_size();
}
self.scroller.scroll_to_top();
}
pub fn set_body_block(&mut self, text_block: TextBlock) {
self.body_content = PanelContent::Block(Box::new(text_block));
if self.auto_size {
self.adjust_size();
}
self.scroller.scroll_to_top();
}
pub fn body_block_mut(&mut self) -> Option<&mut TextBlock> {
match &mut self.body_content {
PanelContent::Block(block) => Some(block.as_mut()),
PanelContent::Text(_) => None,
}
}
pub fn body_block(&self) -> Option<&TextBlock> {
match &self.body_content {
PanelContent::Block(block) => Some(block.as_ref()),
PanelContent::Text(_) => None,
}
}
pub fn with_scrollable(mut self, scrollable: bool) -> Self {
self.scrollable = scrollable;
self
}
pub fn with_scroll_indicators(mut self, show: bool) -> Self {
self.show_scroll_indicators = show;
self
}
pub fn with_scroll_direction(mut self, natural: bool) -> Self {
self.scroller.set_invert_scroll_vertical(!natural);
self
}
pub fn set_invert_scroll(&mut self, invert: bool) {
self.scroller.set_invert_scroll_vertical(invert);
}
pub fn is_scroll_inverted(&self) -> bool {
self.scroller.is_scroll_vertical_inverted()
}
pub fn scroll_by(&mut self, delta: i16) {
if !self.scrollable {
return;
}
let max_offset = self.max_scroll_offset();
self.scroller.scroll_by(delta, max_offset);
}
pub fn scroll_to(&mut self, line: u16) {
if !self.scrollable {
return;
}
let max_offset = self.max_scroll_offset();
self.scroller.scroll_to(line, max_offset);
}
pub fn scroll_to_top(&mut self) {
self.scroller.scroll_to_top();
}
pub fn scroll_to_bottom(&mut self) {
if self.scrollable {
let max_offset = self.max_scroll_offset();
self.scroller.scroll_to_bottom(max_offset);
}
}
pub fn scroll_offset(&self) -> u16 {
self.scroller.offset()
}
pub fn max_scroll_offset(&self) -> u16 {
let (_, inner_height) = self.get_inner_dimensions();
let content_lines = self.get_content_line_count();
content_lines.saturating_sub(inner_height)
}
pub fn can_scroll(&self) -> bool {
if !self.scrollable {
return false;
}
let (_, inner_height) = self.get_inner_dimensions();
self.get_content_line_count() > inner_height
}
pub fn can_scroll_up(&self) -> bool {
self.scrollable && self.scroller.can_scroll_up()
}
pub fn can_scroll_down(&self) -> bool {
self.scrollable && self.scroller.can_scroll_down(self.max_scroll_offset())
}
pub fn handle_scroll_event(&mut self, delta: i8) -> bool {
if !self.scrollable {
return false;
}
let max_offset = self.max_scroll_offset();
self.scroller.handle_scroll_event(delta, max_offset)
}
pub fn handle_drag_event(&mut self, drag_x: u16, drag_y: u16) -> bool {
if !self.scrollable || !self.can_scroll() {
return false;
}
let scrollbar_x = self.width - 2;
let (_, inner_height) = self.get_inner_dimensions();
let content_start_y = if !self.header_text.is_empty() {
3 + self.padding } else {
1 + self.padding };
let max_scroll = self.max_scroll_offset();
self.scroller.handle_drag_event(
drag_x,
drag_y,
scrollbar_x,
inner_height,
content_start_y,
max_scroll,
)
}
pub fn handle_click_event(&mut self, click_x: u16, click_y: u16) -> bool {
if !self.scrollable || !self.can_scroll() {
return false;
}
let scrollbar_x = self.width - 2;
let (_, inner_height) = self.get_inner_dimensions();
let content_start_y = if !self.header_text.is_empty() {
3 + self.padding } else {
1 + self.padding };
let max_scroll = self.max_scroll_offset();
self.scroller.handle_scrollbar_event(
click_x,
click_y,
scrollbar_x,
inner_height,
content_start_y,
max_scroll,
)
}
fn get_content_line_count(&self) -> u16 {
match &self.body_content {
PanelContent::Text(text) => {
if text.is_empty() {
0
} else {
text.lines().count() as u16
}
}
PanelContent::Block(block) => block.line_count() as u16,
}
}
pub fn with_auto_size(mut self, auto_size: bool) -> Self {
self.auto_size = auto_size;
if auto_size {
self.adjust_size();
}
self
}
fn adjust_size(&mut self) {
if !self.auto_size {
return;
}
let mut content_width = 0u16;
let mut content_height = 0u16;
if !self.header_text.is_empty() {
content_width = content_width.max(self.header_text.len() as u16);
content_height += 1; }
match &self.body_content {
PanelContent::Text(text) => {
if !text.is_empty() {
let lines: Vec<&str> = text.lines().collect();
let max_line_width =
lines.iter().map(|line| line.len()).max().unwrap_or(0) as u16;
content_width = content_width.max(max_line_width);
content_height += lines.len() as u16;
}
}
PanelContent::Block(text_block) => {
let (block_width, block_height) = text_block.get_size();
content_width = content_width.max(block_width);
content_height += block_height;
}
}
self.width = content_width + (self.padding * 2) + 2; self.height = content_height + (self.padding * 2) + 2;
if !self.header_text.is_empty() && content_height > 1 {
self.height += 1; }
}
fn get_inner_dimensions(&self) -> (u16, u16) {
let inner_width = self.width.saturating_sub(2 + (self.padding * 2));
let mut inner_height = self.height.saturating_sub(2 + (self.padding * 2));
if !self.header_text.is_empty() {
inner_height = inner_height.saturating_sub(2); }
(inner_width, inner_height)
}
}
impl Widget for Panel {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
if self.width == 0 || self.height == 0 {
return Ok(()); }
let mut current_y = 0u16;
if !self.header_text.is_empty() {
self.draw_header_top_border(window, current_y)?;
current_y += 1;
self.draw_header_text(window, current_y)?;
current_y += 1;
self.draw_header_separator(window, current_y)?;
current_y += 1;
} else {
self.draw_body_top_border(window, current_y)?;
current_y += 1;
}
self.draw_body_content(window, current_y)?;
self.draw_bottom_border(window, self.height - 1)?;
Ok(())
}
fn get_size(&self) -> (u16, u16) {
(self.width, self.height)
}
fn get_position(&self) -> (u16, u16) {
(0, 0) }
}
impl Panel {
fn draw_header_top_border(&self, window: &mut dyn Window, y: u16) -> Result<()> {
let color = self.header_border_color;
self.draw_horizontal_line(window, y, &self.header_style, true, color)
}
fn draw_header_text(&self, window: &mut dyn Window, y: u16) -> Result<()> {
if let Some(color) = self.header_border_color {
window.write_str_colored(y, 0, &self.header_style.vertical.to_string(), color)?;
} else {
window.write_str(y, 0, &self.header_style.vertical.to_string())?;
}
let inner_width = self.width.saturating_sub(2);
let text_len = self.header_text.len() as u16;
let start_x = if text_len < inner_width {
1 + (inner_width - text_len) / 2
} else {
1
};
if let Some(color) = self.header_color {
window.write_str_colored(y, start_x, &self.header_text, color)?;
} else {
window.write_str(y, start_x, &self.header_text)?;
}
if let Some(color) = self.header_border_color {
window.write_str_colored(
y,
self.width - 1,
&self.header_style.vertical.to_string(),
color,
)?;
} else {
window.write_str(y, self.width - 1, &self.header_style.vertical.to_string())?;
}
Ok(())
}
fn draw_header_separator(&self, window: &mut dyn Window, y: u16) -> Result<()> {
let color = self.header_border_color;
if let Some(color) = color {
window.write_str_colored(y, 0, &self.header_style.intersect_left.to_string(), color)?;
} else {
window.write_str(y, 0, &self.header_style.intersect_left.to_string())?;
}
for x in 1..self.width - 1 {
if let Some(color) = color {
window.write_str_colored(y, x, &self.header_style.horizontal.to_string(), color)?;
} else {
window.write_str(y, x, &self.header_style.horizontal.to_string())?;
}
}
if let Some(color) = color {
window.write_str_colored(
y,
self.width - 1,
&self.header_style.intersect_right.to_string(),
color,
)?;
} else {
window.write_str(
y,
self.width - 1,
&self.header_style.intersect_right.to_string(),
)?;
}
Ok(())
}
fn draw_body_top_border(&self, window: &mut dyn Window, y: u16) -> Result<()> {
let color = self.body_border_color;
self.draw_horizontal_line(window, y, &self.body_style, true, color)
}
fn draw_body_content(&self, window: &mut dyn Window, start_y: u16) -> Result<()> {
let (inner_width, inner_height) = self.get_inner_dimensions();
for y_offset in 0..inner_height + (self.padding * 2) {
let y = start_y + y_offset;
if let Some(color) = self.body_border_color {
window.write_str_colored(y, 0, &self.body_style.vertical.to_string(), color)?;
} else {
window.write_str(y, 0, &self.body_style.vertical.to_string())?;
}
if let Some(color) = self.body_border_color {
window.write_str_colored(
y,
self.width - 1,
&self.body_style.vertical.to_string(),
color,
)?;
} else {
window.write_str(y, self.width - 1, &self.body_style.vertical.to_string())?;
}
}
if self.show_scroll_indicators && self.scrollable {
let indicator_color = ColorPair::new(Color::DarkGray, Color::Transparent);
if self.can_scroll() {
let max_scroll = self.max_scroll_offset();
if max_scroll > 0 {
let scrollbar_height = inner_height;
let scrollbar_x = self.width - 2;
let scrollbar_start_y = start_y + self.padding;
let content_lines = self.get_content_line_count();
let thumb_size = ((inner_height as f32 / content_lines as f32)
* scrollbar_height as f32)
.max(1.0) as u16;
let thumb_pos = ((self.scroller.offset() as f32 / max_scroll as f32)
* (scrollbar_height - thumb_size) as f32)
as u16;
for i in 0..scrollbar_height {
let y = scrollbar_start_y + i;
if i >= thumb_pos && i < thumb_pos + thumb_size {
window.write_str_colored(y, scrollbar_x, "█", indicator_color)?;
} else {
window.write_str_colored(
y,
scrollbar_x,
"│",
ColorPair::new(Color::DarkGray, Color::Transparent),
)?;
}
}
if self.can_scroll_up() {
window.write_str_colored(
scrollbar_start_y,
scrollbar_x,
"▲",
indicator_color,
)?;
}
if self.can_scroll_down() {
let scrollbar_end_y =
scrollbar_start_y + scrollbar_height.saturating_sub(1);
window.write_str_colored(
scrollbar_end_y,
scrollbar_x,
"▼",
indicator_color,
)?;
}
}
}
}
let content_start_x = 1 + self.padding;
let content_start_y = start_y + self.padding;
let content_width = inner_width;
let content_height = inner_height;
if content_width > 0 && content_height > 0 {
let mut content_window = WindowView {
window,
x_offset: content_start_x,
y_offset: content_start_y,
width: content_width,
height: content_height,
};
match &self.body_content {
PanelContent::Text(text) => {
self.draw_text_content(&mut content_window, text)?;
}
PanelContent::Block(text_block) => {
text_block.draw(&mut content_window)?;
}
}
}
Ok(())
}
fn draw_text_content(&self, window: &mut dyn Window, text: &str) -> Result<()> {
let lines: Vec<&str> = text.lines().collect();
let (window_width, window_height) = window.get_size();
let start_line = if self.scrollable {
self.scroller.offset() as usize
} else {
0
};
let visible_lines = lines.iter().skip(start_line).take(window_height as usize);
for (i, line) in visible_lines.enumerate() {
let x_pos = match self.alignment {
Alignment::Left => 0,
Alignment::Center => {
let line_len = line.len() as u16;
if line_len < window_width {
(window_width - line_len) / 2
} else {
0
}
}
Alignment::Right => {
let line_len = line.len() as u16;
if line_len < window_width {
window_width - line_len
} else {
0
}
}
};
if let Some(color) = self.body_color {
window.write_str_colored(i as u16, x_pos, line, color)?;
} else {
window.write_str(i as u16, x_pos, line)?;
}
}
Ok(())
}
fn draw_bottom_border(&self, window: &mut dyn Window, y: u16) -> Result<()> {
let color = self.body_border_color;
self.draw_horizontal_line(window, y, &self.body_style, false, color)
}
fn draw_horizontal_line(
&self,
window: &mut dyn Window,
y: u16,
style: &BorderChars,
is_top: bool,
color: Option<ColorPair>,
) -> Result<()> {
let left_char = if is_top {
style.top_left
} else {
style.bottom_left
};
let right_char = if is_top {
style.top_right
} else {
style.bottom_right
};
if let Some(color) = color {
window.write_str_colored(y, 0, &left_char.to_string(), color)?;
} else {
window.write_str(y, 0, &left_char.to_string())?;
}
for x in 1..self.width - 1 {
if let Some(color) = color {
window.write_str_colored(y, x, &style.horizontal.to_string(), color)?;
} else {
window.write_str(y, x, &style.horizontal.to_string())?;
}
}
if let Some(color) = color {
window.write_str_colored(y, self.width - 1, &right_char.to_string(), color)?;
} else {
window.write_str(y, self.width - 1, &right_char.to_string())?;
}
Ok(())
}
}