use std::cmp::min;
use crate::console::{Console, ConsoleOptions, Renderable};
use crate::measure::Measurement;
use crate::segment::Segment;
use crate::text::Text;
#[derive(Debug, Clone)]
pub struct Constrain {
pub renderable: Text,
pub width: Option<usize>,
}
impl Constrain {
pub fn new(renderable: Text, width: Option<usize>) -> Self {
Constrain { renderable, width }
}
#[must_use]
pub fn with_width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
pub fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
let measurement = if let Some(w) = self.width {
let constrained = options.update_width(w);
self.renderable
.measure()
.with_maximum(constrained.max_width)
} else {
self.renderable.measure()
};
measurement.with_maximum(options.max_width)
}
}
impl Renderable for Constrain {
fn rich_console(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
match self.width {
None => self.renderable.rich_console(console, options),
Some(w) => {
let constrained_width = min(w, options.max_width);
let child_options = options.update_width(constrained_width);
self.renderable.rich_console(console, &child_options)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Style;
fn make_console(width: usize) -> Console {
Console::builder()
.width(width)
.force_terminal(true)
.no_color(true)
.markup(false)
.build()
}
fn segments_to_text(segments: &[Segment]) -> String {
segments.iter().map(|s| s.text.as_str()).collect()
}
#[test]
fn test_default_construction() {
let text = Text::new("Hello, world!", Style::null());
let c = Constrain::new(text.clone(), Some(80));
assert_eq!(c.width, Some(80));
assert_eq!(c.renderable.plain(), "Hello, world!");
}
#[test]
fn test_none_width_construction() {
let text = Text::new("Hello", Style::null());
let c = Constrain::new(text, None);
assert_eq!(c.width, None);
}
#[test]
fn test_builder_method() {
let text = Text::new("Hello", Style::null());
let c = Constrain::new(text, None).with_width(40);
assert_eq!(c.width, Some(40));
}
#[test]
fn test_none_width_passthrough() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hello, world!", Style::null());
let c = Constrain::new(text.clone(), None);
let constrained_segments = c.rich_console(&console, &opts);
let direct_segments = text.rich_console(&console, &opts);
assert_eq!(
segments_to_text(&constrained_segments),
segments_to_text(&direct_segments),
);
}
#[test]
fn test_width_smaller_than_content() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hello, world!", Style::null());
let c = Constrain::new(text, Some(5));
let segments = c.rich_console(&console, &opts);
let output = segments_to_text(&segments);
for line in output.split('\n') {
if !line.is_empty() {
assert!(
crate::cells::cell_len(line) <= 5,
"Line '{}' exceeds constrained width of 5 (actual: {})",
line,
crate::cells::cell_len(line),
);
}
}
}
#[test]
fn test_width_larger_than_content() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hi", Style::null());
let c = Constrain::new(text.clone(), Some(40));
let constrained_segments = c.rich_console(&console, &opts);
let direct_segments = text.rich_console(&console, &opts);
assert_eq!(
segments_to_text(&constrained_segments),
segments_to_text(&direct_segments),
);
}
#[test]
fn test_width_min_of_width_and_max_width() {
let console = make_console(10);
let opts = console.options();
let text = Text::new("ABCDEFGHIJKLMNOP", Style::null());
let c = Constrain::new(text, Some(20));
let segments = c.rich_console(&console, &opts);
let output = segments_to_text(&segments);
for line in output.split('\n') {
if !line.is_empty() {
assert!(
crate::cells::cell_len(line) <= 10,
"Line '{}' exceeds effective width of 10 (actual: {})",
line,
crate::cells::cell_len(line),
);
}
}
}
#[test]
fn test_measure_with_width() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hello, world!", Style::null());
let c = Constrain::new(text, Some(5));
let m = c.measure(&console, &opts);
assert!(m.maximum <= 5, "Expected max <= 5, got {}", m.maximum);
}
#[test]
fn test_measure_without_width() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hello", Style::null());
let c = Constrain::new(text.clone(), None);
let m = c.measure(&console, &opts);
let text_m = text.measure().with_maximum(opts.max_width);
assert_eq!(m.minimum, text_m.minimum);
assert_eq!(m.maximum, text_m.maximum);
}
#[test]
fn test_measure_width_larger_than_content() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hello", Style::null());
let c = Constrain::new(text, Some(40));
let m = c.measure(&console, &opts);
assert_eq!(m.maximum, 5);
}
#[test]
fn test_measure_width_smaller_than_console() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hello, world! This is a long sentence.", Style::null());
let c = Constrain::new(text, Some(10));
let m = c.measure(&console, &opts);
assert!(m.maximum <= 10, "Expected max <= 10, got {}", m.maximum,);
}
#[test]
fn test_styled_content_preserved() {
let console = make_console(80);
let opts = console.options();
let text = Text::styled("Bold text", Style::parse("bold").unwrap());
let c = Constrain::new(text, Some(40));
let segments = c.rich_console(&console, &opts);
let has_styled = segments
.iter()
.any(|s| s.text.contains("Bold text") && s.style.is_some());
assert!(has_styled, "Expected styled segment in output");
}
#[test]
fn test_clone() {
let text = Text::new("Hello", Style::null());
let c = Constrain::new(text, Some(40));
let cloned = c.clone();
assert_eq!(cloned.width, c.width);
assert_eq!(cloned.renderable.plain(), c.renderable.plain());
}
#[test]
fn test_debug() {
let text = Text::new("Hello", Style::null());
let c = Constrain::new(text, Some(40));
let debug = format!("{:?}", c);
assert!(debug.contains("Constrain"));
assert!(debug.contains("40"));
}
#[test]
fn test_zero_width() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hello", Style::null());
let c = Constrain::new(text, Some(0));
let segments = c.rich_console(&console, &opts);
let output = segments_to_text(&segments);
for line in output.split('\n') {
assert!(
crate::cells::cell_len(line) == 0,
"Expected empty line, got '{}'",
line,
);
}
}
#[test]
fn test_empty_text() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("", Style::null());
let c = Constrain::new(text, Some(40));
let segments = c.rich_console(&console, &opts);
let output = segments_to_text(&segments);
assert!(output.trim().is_empty());
}
#[test]
fn test_width_equal_to_content() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hello", Style::null());
let c = Constrain::new(text, Some(5));
let segments = c.rich_console(&console, &opts);
let output = segments_to_text(&segments);
let content_lines: Vec<&str> = output.split('\n').filter(|l| !l.is_empty()).collect();
assert_eq!(content_lines.len(), 1);
assert_eq!(content_lines[0], "Hello");
}
}