use super::Widget;
use crate::text::{TabPolicy, cell_width, clip_to_cells, fit_to_cells};
use crate::{Color, ColorPair, Result, Window};
use std::cell::{Ref, RefCell};
#[derive(Debug, Clone, Copy)]
pub enum Alignment {
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy)]
pub enum VerticalAlignment {
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
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
}
pub fn cell_width(&self) -> u16 {
crate::text::cell_width(&self.text, crate::text::TabPolicy::SingleCell) as u16
}
}
impl Widget for Label {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
let (window_width, _) = window.get_size();
let text_w = cell_width(&self.text, TabPolicy::SingleCell) as u16;
let x_pos = match self.alignment {
Alignment::Left => 0,
Alignment::Center => {
if text_w < window_width {
(window_width - text_w) / 2
} else {
0
}
}
Alignment::Right => {
if text_w < window_width {
window_width - text_w
} else {
0
}
}
};
let max_cells = window_width.saturating_sub(x_pos);
let clipped = clip_to_cells(&self.text, max_cells, TabPolicy::SingleCell);
match self.colors {
Some(colors) => window.write_str_colored(0, x_pos, &clipped, colors),
None => window.write_str(0, x_pos, &clipped),
}
}
fn get_size(&self) -> (u16, u16) {
(cell_width(&self.text, TabPolicy::SingleCell) 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
}
pub fn cell_width(&self) -> u16 {
crate::text::cell_width(&self.text, crate::text::TabPolicy::SingleCell) as u16
}
}
impl Widget for Text {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
let (available_width, _) = window.get_size();
let text_w = cell_width(&self.text, TabPolicy::SingleCell) as u16;
let x_pos = match self.alignment {
Alignment::Left => 0,
Alignment::Center => {
if text_w < available_width {
(available_width - text_w) / 2
} else {
0
}
}
Alignment::Right => {
if text_w < available_width {
available_width - text_w
} else {
0
}
}
};
let max_cells = available_width.saturating_sub(x_pos);
let clipped = clip_to_cells(&self.text, max_cells, TabPolicy::SingleCell);
match self.colors {
Some(colors) => window.write_str_colored(0, x_pos, &clipped, colors),
None => window.write_str(0, x_pos, &clipped),
}
}
fn get_size(&self) -> (u16, u16) {
(cell_width(&self.text, TabPolicy::SingleCell) 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,
text_revision: u64,
wrap_cache: RefCell<TextBlockWrapCache>,
}
#[derive(Default)]
struct TextBlockWrapCache {
text_revision: u64,
width: u16,
wrap_mode: Option<TextWrapMode>,
lines: Vec<String>,
valid: bool,
}
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,
text_revision: 0,
wrap_cache: RefCell::new(TextBlockWrapCache::default()),
}
}
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.wrap_cache.get_mut().valid = false;
self
}
pub fn with_word_wrap(mut self) -> Self {
self.wrap_mode = TextWrapMode::WrapWords;
self.wrap_cache.get_mut().valid = false;
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 set_text(&mut self, text: impl Into<String>) {
self.text = text.into();
self.text_revision = self.text_revision.wrapping_add(1);
self.wrap_cache.get_mut().valid = false;
}
pub fn text(&self) -> &str {
&self.text
}
fn rebuild_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
}
}
}
fn wrapped_lines(&self) -> Ref<'_, Vec<String>> {
{
let cache = self.wrap_cache.borrow();
if cache.valid
&& cache.text_revision == self.text_revision
&& cache.width == self.width
&& cache.wrap_mode == Some(self.wrap_mode)
{
return Ref::map(cache, |cache| &cache.lines);
}
}
let lines = self.rebuild_wrapped_lines();
let mut cache = self.wrap_cache.borrow_mut();
cache.text_revision = self.text_revision;
cache.width = self.width;
cache.wrap_mode = Some(self.wrap_mode);
cache.lines = lines;
cache.valid = true;
drop(cache);
Ref::map(RefCell::borrow(&self.wrap_cache), |cache| &cache.lines)
}
}
impl Widget for TextBlock {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
let lines = self.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
}
}
};
for (i, line) in lines.iter().take(available_height as usize).enumerate() {
let line_y = start_y + i as u16;
if line_y >= available_height {
break;
}
let fitted = fit_to_cells(line, available_width, TabPolicy::SingleCell, true);
let line_x = match self.h_align {
Alignment::Left => 0,
Alignment::Center => {
let w = crate::text::cell_width(&fitted, TabPolicy::SingleCell) as u16;
if w < available_width {
(available_width - w) / 2
} else {
0
}
}
Alignment::Right => {
let w = crate::text::cell_width(&fitted, TabPolicy::SingleCell) as u16;
if w < available_width {
available_width - w
} else {
0
}
}
};
if let Some(colors) = self.colors {
window.write_str_colored(line_y, line_x, &fitted, colors)?;
} else {
window.write_str(line_y, line_x, &fitted)?;
}
}
Ok(())
}
fn get_size(&self) -> (u16, u16) {
(self.width, self.height)
}
fn get_position(&self) -> (u16, u16) {
(0, 0) }
}