use std::ops::ControlFlow;
use anathema_geometry::{LocalPos, Size};
use anathema_value_resolver::{AttributeStorage, ValueKind};
use anathema_widgets::error::Result;
use anathema_widgets::layout::text::{ProcessResult, Segment, Strings};
use anathema_widgets::layout::{Constraints, LayoutCtx, PositionCtx};
use anathema_widgets::paint::{Glyphs, PaintCtx, SizePos};
use anathema_widgets::{LayoutForEach, PaintChildren, PositionChildren, Widget, WidgetId};
use crate::{LEFT, RIGHT};
pub(crate) const WRAP: &str = "wrap";
pub(crate) const TEXT_ALIGN: &str = "text_align";
#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)]
pub enum TextAlignment {
#[default]
Left,
Centre,
Right,
}
impl TryFrom<&ValueKind<'_>> for TextAlignment {
type Error = ();
fn try_from(value: &ValueKind<'_>) -> Result<Self, Self::Error> {
let s = value.as_str().ok_or(())?;
match s {
LEFT => Ok(TextAlignment::Left),
RIGHT => Ok(TextAlignment::Right),
"centre" | "center" => Ok(TextAlignment::Centre),
_ => Err(()),
}
}
}
#[derive(Debug, Default)]
pub struct Text {
strings: Strings,
}
impl Widget for Text {
fn layout<'bp>(
&mut self,
mut children: LayoutForEach<'_, 'bp>,
constraints: Constraints,
id: WidgetId,
ctx: &mut LayoutCtx<'_, 'bp>,
) -> Result<Size> {
let attributes = ctx.attributes(id);
let wrap = attributes.get_as(WRAP).unwrap_or_default();
let size = constraints.max_size();
self.strings = Strings::new(size, wrap);
self.strings.set_style(id);
if let Some(text) = attributes.value() {
text.strings(|s| match self.strings.add_str(s) {
ProcessResult::Break => false,
ProcessResult::Continue => true,
});
}
_ = children.each(ctx, |ctx, child, _| {
let Some(_span) = child.try_to_ref::<Span>() else {
return Ok(ControlFlow::Continue(()));
};
self.strings.set_style(child.id());
let attributes = ctx.attributes(child.id());
if let Some(text) = attributes.value() {
text.strings(|s| match self.strings.add_str(s) {
ProcessResult::Break => false,
ProcessResult::Continue => true,
});
Ok(ControlFlow::Continue(()))
} else {
Ok(ControlFlow::Break(()))
}
})?;
Ok(self.strings.finish())
}
fn paint<'bp>(
&mut self,
_: PaintChildren<'_, 'bp>,
id: WidgetId,
attribute_storage: &AttributeStorage<'bp>,
mut ctx: PaintCtx<'_, SizePos>,
) {
let lines = self.strings.lines();
let alignment = attribute_storage.get(id).get_as(TEXT_ALIGN).unwrap_or_default();
let mut pos = LocalPos::ZERO;
let mut style = attribute_storage.get(id);
for line in lines {
let x = match alignment {
TextAlignment::Left => 0,
TextAlignment::Centre => ctx.local_size.width / 2 - line.width / 2,
TextAlignment::Right => ctx.local_size.width - line.width,
};
pos.x = x;
for entry in line.entries {
match entry {
Segment::Str(s) => {
let glyphs = Glyphs::new(s);
if let Some(new_pos) = ctx.place_glyphs(glyphs, pos) {
for x in pos.x..new_pos.x {
ctx.set_attributes(style, (x, pos.y).into());
}
pos = new_pos;
}
}
Segment::SetStyle(attribute_id) => style = attribute_storage.get(attribute_id),
}
}
pos.y += 1;
pos.x = 0;
}
}
fn position<'bp>(&mut self, _: PositionChildren<'_, 'bp>, _: WidgetId, _: &AttributeStorage<'bp>, _: PositionCtx) {
}
}
#[derive(Default, Copy, Clone)]
pub struct Span;
impl Widget for Span {
fn layout<'bp>(
&mut self,
_: LayoutForEach<'_, 'bp>,
_: Constraints,
_: WidgetId,
_: &mut LayoutCtx<'_, 'bp>,
) -> Result<Size> {
Ok(Size::ZERO)
}
fn position<'bp>(&mut self, _: PositionChildren<'_, 'bp>, _: WidgetId, _: &AttributeStorage<'bp>, _: PositionCtx) {
}
}
#[cfg(test)]
mod test {
use crate::testing::TestRunner;
#[test]
fn word_wrap_excessive_space() {
let src = "text 'hello how are you'";
let expected = "
╔════════════════╗
║hello how ║
║are you ║
║ ║
║ ║
║ ║
║ ║
╚════════════════╝";
TestRunner::new(src, (16, 6)).instance().render_assert(expected);
}
#[test]
fn word_wrap() {
let src = "text 'hello how are you'";
let expected = r#"
╔════════════════╗
║hello how are ║
║you ║
║ ║
╚════════════════╝
"#;
TestRunner::new(src, (16, 3)).instance().render_assert(expected);
}
#[test]
fn break_word_wrap() {
let src = "text [wrap: 'break'] 'hello howareyoudoing'";
let expected = r#"
╔════════════════╗
║hello howareyoud║
║oing ║
║ ║
╚════════════════╝
"#;
TestRunner::new(src, (16, 3)).instance().render_assert(expected);
}
#[test]
fn char_wrap_layout_multiple_spans() {
let src = r#"
text 'one'
span 'two'
span ' averylongword'
span ' bunny'
"#;
let expected = r#"
╔═══════════════════╗
║onetwo ║
║averylongword bunny║
║ ║
╚═══════════════════╝
"#;
TestRunner::new(src, (19, 3)).instance().render_assert(expected);
}
#[test]
fn multi_line_with_span() {
let src = r#"
border [width: 5 + 2]
text 'one'
span 'two'
"#;
let expected = r#"
╔═════════╗
║┌─────┐ ║
║│onetw│ ║
║│o │ ║
║└─────┘ ║
╚═════════╝
"#;
TestRunner::new(src, (9, 4)).instance().render_assert(expected);
}
#[test]
fn right_alignment() {
let src = "text [text_align: 'right'] 'a one xxxxxxxxxxxxxxxxxx'";
let expected = r#"
╔══════════════════╗
║ a one ║
║xxxxxxxxxxxxxxxxxx║
║ ║
╚══════════════════╝
"#;
TestRunner::new(src, (18, 3)).instance().render_assert(expected);
}
#[test]
fn centre_alignment() {
let src = "text [text_align: 'centre'] 'a one xxxxxxxxxxxxxxxxxx'";
let expected = r#"
╔══════════════════╗
║ a one ║
║xxxxxxxxxxxxxxxxxx║
║ ║
╚══════════════════╝
"#;
TestRunner::new(src, (18, 3)).instance().render_assert(expected);
}
#[test]
fn line_break() {
let src = "text 'What have you'";
let expected = r#"
╔═════════╗
║What have║
║you ║
║ ║
╚═════════╝
"#;
TestRunner::new(src, (9, 3)).instance().render_assert(expected);
}
}