use super::Widget;
use crate::{Color, ColorPair, Result, Window};
#[derive(Debug, Clone, Copy)]
pub enum Alignment {
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy)]
pub enum VerticalAlignment {
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone, Copy)]
pub enum TextWrapMode {
None,
Wrap,
WrapWords,
}
pub struct Label {
text: String,
colors: Option<ColorPair>,
alignment: Alignment,
}
impl Label {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
colors: None,
alignment: Alignment::Left,
}
}
pub fn with_color(mut self, colors: ColorPair) -> Self {
self.colors = Some(colors);
self
}
pub fn with_text_color(mut self, color: Color) -> Self {
self.colors = Some(ColorPair::new(color, Color::Transparent));
self
}
pub fn with_alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn set_text(&mut self, text: impl Into<String>) {
self.text = text.into();
}
pub fn text(&self) -> &str {
&self.text
}
pub fn get_length(&self) -> u16 {
self.text.chars().count() as u16
}
fn calculate_aligned_x(&self, available_width: u16) -> u16 {
let text_length = self.get_length();
match self.alignment {
Alignment::Left => 0,
Alignment::Center => {
if text_length < available_width {
(available_width - text_length) / 2
} else {
0
}
}
Alignment::Right => {
if text_length < available_width {
available_width - text_length
} else {
0
}
}
}
}
}
impl Widget for Label {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
let (window_width, _) = window.get_size();
let x_pos = self.calculate_aligned_x(window_width);
match self.colors {
Some(colors) => window.write_str_colored(0, x_pos, &self.text, colors),
None => window.write_str(0, x_pos, &self.text),
}
}
fn get_size(&self) -> (u16, u16) {
(self.text.chars().count() as u16, 1)
}
fn get_position(&self) -> (u16, u16) {
(0, 0) }
}
pub struct Text {
text: String,
colors: Option<ColorPair>,
alignment: Alignment,
}
impl Text {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
colors: None,
alignment: Alignment::Left,
}
}
pub fn with_color(mut self, colors: ColorPair) -> Self {
self.colors = Some(colors);
self
}
pub fn with_text_color(mut self, color: Color) -> Self {
self.colors = Some(ColorPair::new(color, Color::Transparent));
self
}
pub fn with_alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn set_text(&mut self, text: impl Into<String>) {
self.text = text.into();
}
pub fn text(&self) -> &str {
&self.text
}
pub fn get_length(&self) -> u16 {
self.text.chars().count() as u16
}
fn calculate_aligned_x(&self, available_width: u16) -> u16 {
let text_length = self.get_length();
match self.alignment {
Alignment::Left => 0,
Alignment::Center => {
if text_length < available_width {
(available_width - text_length) / 2
} else {
0
}
}
Alignment::Right => {
if text_length < available_width {
available_width - text_length
} else {
0
}
}
}
}
}
impl Widget for Text {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
let (available_width, _) = window.get_size();
let x_pos = self.calculate_aligned_x(available_width);
match self.colors {
Some(colors) => window.write_str_colored(0, x_pos, &self.text, colors),
None => window.write_str(0, x_pos, &self.text),
}
}
fn get_size(&self) -> (u16, u16) {
(self.text.chars().count() as u16, 1)
}
fn get_position(&self) -> (u16, u16) {
(0, 0) }
}
pub struct TextBlock {
width: u16,
height: u16,
text: String,
colors: Option<ColorPair>,
wrap_mode: TextWrapMode,
h_align: Alignment,
v_align: VerticalAlignment,
scroll_offset: u16,
}
impl TextBlock {
pub fn new(width: u16, height: u16, text: impl Into<String>) -> Self {
Self {
width,
height,
text: text.into(),
colors: None,
wrap_mode: TextWrapMode::Wrap,
h_align: Alignment::Left,
v_align: VerticalAlignment::Top,
scroll_offset: 0,
}
}
pub fn auto_sized(text: impl Into<String>) -> Self {
let text = text.into();
let lines: Vec<&str> = text.lines().collect();
let width = lines.iter().map(|line| line.len()).max().unwrap_or(0) as u16;
let height = lines.len() as u16;
Self::new(width, height, text)
}
pub fn auto_sized_with_word_wrap(text: impl Into<String>, max_width: u16) -> Self {
let text = text.into();
let mut lines = Vec::new();
let mut current_line = String::new();
for word in text.split_whitespace() {
let needed_space = if current_line.is_empty() {
word.len()
} else {
current_line.len() + 1 + word.len()
};
if needed_space <= max_width as usize {
if !current_line.is_empty() {
current_line.push(' ');
}
current_line.push_str(word);
} else {
if !current_line.is_empty() {
lines.push(current_line);
}
current_line = word.to_string();
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
let actual_width = lines.iter().map(|line| line.len()).max().unwrap_or(0) as u16;
let height = lines.len() as u16;
let mut text_block = Self::new(actual_width, height, lines.join("\n"));
text_block.wrap_mode = TextWrapMode::None; text_block
}
pub fn with_colors(mut self, colors: ColorPair) -> Self {
self.colors = Some(colors);
self
}
pub fn with_text_color(mut self, color: Color) -> Self {
self.colors = Some(ColorPair::new(color, Color::Transparent));
self
}
pub fn with_wrap_mode(mut self, mode: TextWrapMode) -> Self {
self.wrap_mode = mode;
self
}
pub fn with_word_wrap(mut self) -> Self {
self.wrap_mode = TextWrapMode::WrapWords;
self
}
pub fn with_alignment(mut self, h_align: Alignment, v_align: VerticalAlignment) -> Self {
self.h_align = h_align;
self.v_align = v_align;
self
}
pub fn scroll_to(&mut self, line: u16) {
self.scroll_offset = line;
}
pub fn scroll_by(&mut self, delta: i16) {
self.scroll_offset = if delta.is_negative() {
self.scroll_offset.saturating_sub(delta.abs() as u16)
} else {
self.scroll_offset.saturating_add(delta as u16)
};
}
pub fn set_text(&mut self, text: impl Into<String>) {
self.text = text.into();
}
pub fn text(&self) -> &str {
&self.text
}
fn get_wrapped_lines(&self) -> Vec<String> {
match self.wrap_mode {
TextWrapMode::None => self.text.lines().map(String::from).collect(),
TextWrapMode::Wrap => self
.text
.chars()
.collect::<Vec<_>>()
.chunks(self.width as usize)
.map(|chunk| chunk.iter().collect::<String>())
.collect(),
TextWrapMode::WrapWords => {
let mut lines = Vec::new();
let mut current_line = String::new();
for word in self.text.split_whitespace() {
if current_line.len() + word.len() + 1 <= self.width as usize {
if !current_line.is_empty() {
current_line.push(' ');
}
current_line.push_str(word);
} else {
if !current_line.is_empty() {
lines.push(current_line);
}
current_line = word.to_string();
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
lines
}
}
}
}
impl Widget for TextBlock {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
let lines = self.get_wrapped_lines();
let (window_width, window_height) = window.get_size();
let available_width = window_width.min(self.width);
let available_height = window_height.min(self.height);
let total_lines = lines.len().min(available_height as usize);
let start_y = match self.v_align {
VerticalAlignment::Top => 0,
VerticalAlignment::Middle => {
if total_lines < available_height as usize {
(available_height - total_lines as u16) / 2
} else {
0
}
}
VerticalAlignment::Bottom => {
if total_lines < available_height as usize {
available_height - total_lines as u16
} else {
0
}
}
};
let start_line = self.scroll_offset as usize;
let display_lines: Vec<String> = lines
.into_iter()
.skip(start_line)
.take(available_height as usize)
.collect();
for (i, line) in display_lines.iter().enumerate() {
let line_y = start_y + i as u16;
if line_y >= available_height {
break;
}
let line_x = match self.h_align {
Alignment::Left => 0,
Alignment::Center => {
if line.len() < available_width as usize {
(available_width - line.len() as u16) / 2
} else {
0
}
}
Alignment::Right => {
if line.len() < available_width as usize {
available_width - line.len() as u16
} else {
0
}
}
};
if let Some(colors) = self.colors {
window.write_str_colored(line_y, line_x, line, colors)?;
} else {
window.write_str(line_y, line_x, line)?;
}
}
Ok(())
}
fn get_size(&self) -> (u16, u16) {
(self.width, self.height)
}
fn get_position(&self) -> (u16, u16) {
(0, 0) }
}