#[cfg(feature = "text-table")]
use oxiui_text::{ShapedText, TextPipeline, TextStyle};
use crate::Cell;
#[cfg(feature = "text-table")]
#[derive(Clone, Debug)]
pub struct StyledSpan {
pub text: String,
pub style: TextStyle,
}
#[cfg(not(feature = "text-table"))]
#[derive(Clone, Debug)]
pub struct StyledSpan {
pub text: String,
}
#[derive(Clone, Debug, Default)]
pub struct RichCell {
spans: Vec<StyledSpan>,
}
impl RichCell {
pub fn new() -> Self {
Self::default()
}
#[cfg(feature = "text-table")]
pub fn plain(text: impl Into<String>) -> Self {
let mut cell = Self::new();
cell.spans.push(StyledSpan {
text: text.into(),
style: TextStyle::default(),
});
cell
}
#[cfg(not(feature = "text-table"))]
pub fn plain(text: impl Into<String>) -> Self {
let mut cell = Self::new();
cell.spans.push(StyledSpan { text: text.into() });
cell
}
pub fn push_span(&mut self, span: StyledSpan) {
self.spans.push(span);
}
pub fn spans(&self) -> &[StyledSpan] {
&self.spans
}
pub fn plain_text(&self) -> String {
self.spans.iter().map(|s| s.text.as_str()).collect()
}
pub fn to_plain_cell(&self) -> Cell {
Cell::Text(self.plain_text())
}
#[cfg(feature = "text-table")]
pub fn shape_spans(&self, pipeline: &mut TextPipeline) -> Result<Vec<ShapedText>, String> {
let mut results = Vec::with_capacity(self.spans.len());
for span in &self.spans {
let shaped = pipeline
.shape(&span.text, &span.style)
.map_err(|e| e.to_string())?;
results.push(shaped);
}
Ok(results)
}
#[cfg(feature = "text-table")]
pub fn measure(&self, pipeline: &mut TextPipeline) -> Result<(f32, f32), String> {
let mut total_w = 0.0_f32;
let mut max_h = 0.0_f32;
for span in &self.spans {
let (w, h) = pipeline
.measure(&span.text, &span.style)
.map_err(|e| e.to_string())?;
total_w += w;
max_h = max_h.max(h);
}
Ok((total_w, max_h))
}
}
pub trait CellRichExt {
fn to_rich_cell(&self) -> RichCell;
}
impl CellRichExt for Cell {
fn to_rich_cell(&self) -> RichCell {
RichCell::plain(self.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rich_cell_default_empty() {
let cell = RichCell::new();
assert!(cell.spans().is_empty());
assert_eq!(cell.plain_text(), "");
}
#[test]
fn rich_cell_plain_single_span() {
let cell = RichCell::plain("hello");
assert_eq!(cell.spans().len(), 1);
assert_eq!(cell.plain_text(), "hello");
}
#[test]
fn rich_cell_multi_span_plain_text() {
let mut cell = RichCell::plain("Hello, ");
cell.push_span(RichCell::plain("δΈη!").spans()[0].clone());
assert_eq!(cell.plain_text(), "Hello, δΈη!");
}
#[test]
fn rich_cell_to_plain_cell_is_text() {
let cell = RichCell::plain("data");
let plain = cell.to_plain_cell();
assert!(matches!(plain, Cell::Text(_)));
assert_eq!(plain.to_string(), "data");
}
#[test]
fn cell_to_rich_cell_int() {
use super::CellRichExt;
let cell = Cell::Int(42);
let rich = cell.to_rich_cell();
assert_eq!(rich.plain_text(), "42");
}
#[test]
fn cell_to_rich_cell_float() {
use super::CellRichExt;
#[allow(clippy::approx_constant)]
let cell = Cell::Float(3.14_f64);
let rich = cell.to_rich_cell();
assert_eq!(rich.plain_text(), "3.14");
}
#[test]
fn cell_to_rich_cell_bool() {
use super::CellRichExt;
let cell = Cell::Bool(true);
let rich = cell.to_rich_cell();
assert_eq!(rich.plain_text(), "true");
}
#[test]
fn cell_to_rich_cell_empty() {
use super::CellRichExt;
let cell = Cell::Empty;
let rich = cell.to_rich_cell();
assert_eq!(rich.plain_text(), "");
}
#[test]
fn rich_cell_cjk_codepoints_preserved() {
let cell = RichCell::plain("ζ₯ζ¬θͺγγΉγ");
assert_eq!(cell.plain_text(), "ζ₯ζ¬θͺγγΉγ");
}
#[test]
fn rich_cell_emoji_codepoints_preserved() {
let cell = RichCell::plain("ππ¦π");
assert_eq!(cell.plain_text(), "ππ¦π");
}
#[test]
fn rich_cell_mixed_cjk_emoji_ascii() {
let mut cell = RichCell::plain("Hello ");
cell.push_span(RichCell::plain("δΈη π").spans()[0].clone());
assert_eq!(cell.plain_text(), "Hello δΈη π");
}
#[cfg(feature = "text-table")]
#[test]
fn styled_span_has_style_field() {
let span = StyledSpan {
text: "test".to_owned(),
style: TextStyle::new(14.0),
};
assert!((span.style.font_size - 14.0).abs() < f32::EPSILON);
}
}