use super::{BorderChars, Widget};
use crate::widgets::common::WindowView;
use crate::{Color, ColorPair, Result, Window};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LayoutDirection {
Vertical,
Horizontal,
}
#[derive(Debug, Clone)]
pub enum BorderStyle {
None,
Visible(BorderChars),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Padding {
pub top: u16,
pub right: u16,
pub bottom: u16,
pub left: u16,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ContentAlignment {
Normal,
AutoCenter,
}
impl Padding {
pub fn uniform(amount: u16) -> Self {
Self {
top: amount,
right: amount,
bottom: amount,
left: amount,
}
}
pub fn symmetric(vertical: u16, horizontal: u16) -> Self {
Self {
top: vertical,
right: horizontal,
bottom: vertical,
left: horizontal,
}
}
pub fn custom(top: u16, right: u16, bottom: u16, left: u16) -> Self {
Self {
top,
right,
bottom,
left,
}
}
pub fn horizontal_total(&self) -> u16 {
self.left + self.right
}
pub fn vertical_total(&self) -> u16 {
self.top + self.bottom
}
}
impl Default for Padding {
fn default() -> Self {
Self::uniform(0)
}
}
pub struct Container {
x: u16,
y: u16,
width: u16,
height: u16,
border_style: BorderStyle,
border_color: Option<ColorPair>,
children: Vec<Box<dyn Widget>>,
layout_direction: LayoutDirection,
padding: Padding,
child_spacing: u16,
auto_size: bool,
fullscreen: bool,
content_alignment: ContentAlignment,
}
impl Container {
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
Self {
x,
y,
width,
height,
border_style: BorderStyle::Visible(BorderChars::single_line()),
border_color: None,
children: Vec::new(),
layout_direction: LayoutDirection::Vertical,
padding: Padding::uniform(1),
child_spacing: 0,
auto_size: width == 0 || height == 0,
fullscreen: false,
content_alignment: ContentAlignment::Normal,
}
}
pub fn fullscreen() -> Self {
Self {
x: 0,
y: 0,
width: 0, height: 0,
border_style: BorderStyle::None,
border_color: None,
children: Vec::new(),
layout_direction: LayoutDirection::Vertical,
padding: Padding::uniform(0),
child_spacing: 0,
auto_size: false,
fullscreen: true,
content_alignment: ContentAlignment::Normal,
}
}
pub fn fullscreen_with_size(width: u16, height: u16) -> Self {
let mut container = Self::fullscreen();
container.update_fullscreen_size(width, height);
container
}
pub fn vertical() -> Self {
Self::new(0, 0, 0, 0).with_layout_direction(LayoutDirection::Vertical)
}
pub fn horizontal() -> Self {
Self::new(0, 0, 0, 0).with_layout_direction(LayoutDirection::Horizontal)
}
pub fn with_border_style(mut self, style: BorderStyle) -> Self {
self.border_style = style;
self
}
pub fn with_border(mut self, chars: BorderChars) -> Self {
self.border_style = BorderStyle::Visible(chars);
self
}
pub fn without_border(mut self) -> Self {
self.border_style = BorderStyle::None;
self
}
pub fn with_border_color(mut self, color: Color) -> Self {
self.border_color = Some(ColorPair::new(color, Color::Transparent));
self
}
pub fn with_layout_direction(mut self, direction: LayoutDirection) -> Self {
self.layout_direction = direction;
self
}
pub fn with_padding(mut self, padding: u16) -> Self {
self.padding = Padding::uniform(padding);
if self.auto_size {
self.calculate_size();
}
self
}
pub fn with_padding_config(mut self, padding: Padding) -> Self {
self.padding = padding;
if self.auto_size {
self.calculate_size();
}
self
}
pub fn with_auto_center(mut self) -> Self {
self.content_alignment = ContentAlignment::AutoCenter;
self
}
pub fn with_child_spacing(mut self, spacing: u16) -> Self {
self.child_spacing = spacing;
if self.auto_size {
self.calculate_size();
}
self
}
pub fn with_auto_size(mut self, auto_size: bool) -> Self {
self.auto_size = auto_size;
if auto_size {
self.calculate_size();
}
self
}
pub fn add_child(mut self, widget: impl Widget + 'static) -> Self {
self.children.push(Box::new(widget));
if self.auto_size {
self.calculate_size();
}
self
}
pub fn add_children(mut self, widgets: Vec<Box<dyn Widget>>) -> Self {
self.children.extend(widgets);
if self.auto_size {
self.calculate_size();
}
self
}
pub fn get_content_size(&self) -> (u16, u16) {
let border_thickness = self.border_thickness();
let content_width = self
.width
.saturating_sub(border_thickness * 2)
.saturating_sub(self.padding.horizontal_total());
let content_height = self
.height
.saturating_sub(border_thickness * 2)
.saturating_sub(self.padding.vertical_total());
(content_width, content_height)
}
fn border_thickness(&self) -> u16 {
match self.border_style {
BorderStyle::None => 0,
BorderStyle::Visible(_) => 1,
}
}
pub fn update_fullscreen_size(&mut self, terminal_width: u16, terminal_height: u16) {
if self.fullscreen {
self.width = terminal_width;
self.height = terminal_height;
}
}
fn get_content_area(&self) -> (u16, u16, u16, u16) {
let border_thickness = self.border_thickness();
let content_x = self.x + border_thickness + self.padding.left;
let content_y = self.y + border_thickness + self.padding.top;
let content_width = self
.width
.saturating_sub(border_thickness * 2)
.saturating_sub(self.padding.horizontal_total());
let content_height = self
.height
.saturating_sub(border_thickness * 2)
.saturating_sub(self.padding.vertical_total());
(content_x, content_y, content_width, content_height)
}
fn calculate_size(&mut self) {
if self.children.is_empty() {
let border_thickness = self.border_thickness();
self.width = border_thickness * 2 + self.padding.horizontal_total();
self.height = border_thickness * 2 + self.padding.vertical_total();
return;
}
let border_thickness = self.border_thickness();
let mut required_width = 0u16;
let mut required_height = 0u16;
match self.layout_direction {
LayoutDirection::Vertical => {
for (i, child) in self.children.iter().enumerate() {
let (child_width, child_height) = child.get_size();
required_width = required_width.max(child_width);
required_height += child_height;
if i < self.children.len() - 1 {
required_height += self.child_spacing;
}
}
}
LayoutDirection::Horizontal => {
for (i, child) in self.children.iter().enumerate() {
let (child_width, child_height) = child.get_size();
required_width += child_width;
required_height = required_height.max(child_height);
if i < self.children.len() - 1 {
required_width += self.child_spacing;
}
}
}
}
self.width = required_width + (border_thickness * 2) + self.padding.horizontal_total();
self.height = required_height + (border_thickness * 2) + self.padding.vertical_total();
}
fn draw_border(&self, window: &mut dyn Window) -> Result<()> {
if let BorderStyle::Visible(border_chars) = &self.border_style {
let mut draw_colored = |y: u16, x: u16, ch: char| -> Result<()> {
if let Some(color) = self.border_color {
window.write_str_colored(y, x, &ch.to_string(), color)
} else {
window.write_str(y, x, &ch.to_string())
}
};
draw_colored(self.y, self.x, border_chars.top_left)?;
draw_colored(self.y, self.x + self.width - 1, border_chars.top_right)?;
draw_colored(self.y + self.height - 1, self.x, border_chars.bottom_left)?;
draw_colored(
self.y + self.height - 1,
self.x + self.width - 1,
border_chars.bottom_right,
)?;
for x in 1..self.width - 1 {
draw_colored(self.y, self.x + x, border_chars.horizontal)?;
draw_colored(
self.y + self.height - 1,
self.x + x,
border_chars.horizontal,
)?;
}
for y in 1..self.height - 1 {
draw_colored(self.y + y, self.x, border_chars.vertical)?;
draw_colored(self.y + y, self.x + self.width - 1, border_chars.vertical)?;
}
}
Ok(())
}
fn draw_children(&self, window: &mut dyn Window) -> Result<()> {
let (content_x, content_y, content_width, content_height) = self.get_content_area();
let (total_content_width, total_content_height) =
if matches!(self.content_alignment, ContentAlignment::AutoCenter) {
self.calculate_total_content_size()
} else {
(0, 0) };
let (start_x, start_y) = match self.content_alignment {
ContentAlignment::Normal => (0, 0),
ContentAlignment::AutoCenter => {
let offset_x = match self.layout_direction {
LayoutDirection::Vertical => {
if total_content_width < content_width {
(content_width - total_content_width) / 2
} else {
0
}
}
LayoutDirection::Horizontal => {
if total_content_width < content_width {
(content_width - total_content_width) / 2
} else {
0
}
}
};
let offset_y = if total_content_height < content_height {
(content_height - total_content_height) / 2
} else {
0
};
(offset_x, offset_y)
}
};
let mut current_x = start_x;
let mut current_y = start_y;
for (i, child) in self.children.iter().enumerate() {
let (child_width, child_height) = child.get_size();
let (child_x, child_y) = match self.content_alignment {
ContentAlignment::Normal => (current_x, current_y),
ContentAlignment::AutoCenter => {
match self.layout_direction {
LayoutDirection::Vertical => {
let centered_x = if child_width < content_width {
let available_space = content_width - child_width;
available_space / 2
} else {
0
};
(centered_x, current_y)
}
LayoutDirection::Horizontal => {
(current_x, start_y)
}
}
}
};
let mut child_window = WindowView {
window,
x_offset: content_x + child_x,
y_offset: content_y + child_y,
width: content_width.saturating_sub(child_x),
height: content_height.saturating_sub(child_y),
};
child.draw(&mut child_window)?;
match self.layout_direction {
LayoutDirection::Vertical => {
current_y += child_height;
if i < self.children.len() - 1 {
current_y += self.child_spacing;
}
}
LayoutDirection::Horizontal => {
current_x += child_width;
if i < self.children.len() - 1 {
current_x += self.child_spacing;
}
}
}
}
Ok(())
}
fn calculate_total_content_size(&self) -> (u16, u16) {
if self.children.is_empty() {
return (0, 0);
}
let mut total_width = 0u16;
let mut total_height = 0u16;
match self.layout_direction {
LayoutDirection::Vertical => {
for (i, child) in self.children.iter().enumerate() {
let (child_width, child_height) = child.get_size();
total_width = total_width.max(child_width);
total_height += child_height;
if i < self.children.len() - 1 {
total_height += self.child_spacing;
}
}
}
LayoutDirection::Horizontal => {
for (i, child) in self.children.iter().enumerate() {
let (child_width, child_height) = child.get_size();
total_width += child_width;
total_height = total_height.max(child_height);
if i < self.children.len() - 1 {
total_width += self.child_spacing;
}
}
}
}
(total_width, total_height)
}
}
impl Widget for Container {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
self.draw_border(window)?;
self.draw_children(window)?;
Ok(())
}
fn get_size(&self) -> (u16, u16) {
if self.fullscreen && (self.width == 0 || self.height == 0) {
(80, 24) } else {
(self.width, self.height)
}
}
fn get_position(&self) -> (u16, u16) {
(self.x, self.y)
}
}
impl Container {
pub fn div() -> Self {
Self::new(0, 0, 0, 0).without_border()
}
pub fn panel() -> Self {
Self::new(0, 0, 0, 0)
.with_border(BorderChars::single_line())
.with_padding(1)
}
pub fn card() -> Self {
Self::new(0, 0, 0, 0)
.with_border(BorderChars::double_line())
.with_padding(2)
}
pub fn centered_text(text: impl Into<String>, terminal_width: u16) -> Self {
use crate::widgets::Text;
let text_string = text.into();
let text_len = text_string.chars().count() as u16;
let left_padding = if terminal_width > text_len {
(terminal_width - text_len) / 2
} else {
0
};
Self::div()
.add_child(Text::new(text_string))
.with_padding_config(Padding::custom(0, 0, 0, left_padding))
}
pub fn centered_colored_text(
text: impl Into<String>,
color: crate::Color,
terminal_width: u16,
) -> Self {
use crate::widgets::Text;
let text_string = text.into();
let text_len = text_string.chars().count() as u16;
let left_padding = if terminal_width > text_len {
(terminal_width - text_len) / 2
} else {
0
};
Self::div()
.add_child(Text::new(text_string).with_text_color(color))
.with_padding_config(Padding::custom(0, 0, 0, left_padding))
}
}