use alloc::string::String;
use alloc::vec::Vec;
use rlvgl_core::draw::draw_widget_bg;
use rlvgl_core::event::Event;
use rlvgl_core::font::{FontId, FontMetrics, WidgetFont, shape_text_ltr, wrap_greedy_ltr};
use rlvgl_core::renderer::{ClipRenderer, Renderer};
use rlvgl_core::style::Style;
use rlvgl_core::widget::{Color, Rect, Widget};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpanId(pub u16);
pub const SPAN_NONE: SpanId = SpanId(u16::MAX);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpanStyle {
pub color: Color,
pub font_id: Option<FontId>,
}
impl SpanStyle {
pub fn with_color(color: Color) -> Self {
Self {
color,
font_id: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SpanOverflow {
#[default]
Clip,
Expand,
}
pub struct Span {
id: SpanId,
text: String,
style: SpanStyle,
}
pub struct Spangroup {
bounds: Rect,
spans: Vec<Span>,
next_id: u16,
overflow: SpanOverflow,
pub style: Style,
font: WidgetFont,
}
impl Spangroup {
pub fn new(bounds: Rect) -> Self {
Self {
bounds,
spans: Vec::new(),
next_id: 0,
overflow: SpanOverflow::default(),
style: Style::default(),
font: WidgetFont::new(),
}
}
pub fn add_span(&mut self, text: &str, style: SpanStyle) -> SpanId {
if self.next_id == u16::MAX {
return SPAN_NONE;
}
let id = SpanId(self.next_id);
self.next_id += 1;
self.spans.push(Span {
id,
text: String::from(text),
style,
});
id
}
pub fn remove_span(&mut self, id: SpanId) {
self.spans.retain(|s| s.id != id);
}
pub fn span_count(&self) -> usize {
self.spans.len()
}
pub fn clear_spans(&mut self) {
self.spans.clear();
self.next_id = 0;
}
pub fn set_span_text(&mut self, id: SpanId, text: &str) {
if let Some(s) = self.spans.iter_mut().find(|s| s.id == id) {
s.text = String::from(text);
}
}
pub fn set_span_style(&mut self, id: SpanId, style: SpanStyle) {
if let Some(s) = self.spans.iter_mut().find(|s| s.id == id) {
s.style = style;
}
}
pub fn set_overflow(&mut self, mode: SpanOverflow) {
self.overflow = mode;
}
pub fn overflow(&self) -> SpanOverflow {
self.overflow
}
pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
self.font.set(font);
}
pub fn content_height(&self) -> i32 {
let font = self.font.resolve();
let concat = self.concat_text();
let wrapped = wrap_greedy_ltr(font, &concat, self.bounds.width.max(1), 0, 0);
wrapped.used_height
}
fn concat_with_offsets(&self) -> (String, Vec<usize>) {
let mut concat = String::new();
let mut offsets = Vec::with_capacity(self.spans.len());
for span in &self.spans {
offsets.push(concat.len());
concat.push_str(&span.text);
}
(concat, offsets)
}
fn concat_text(&self) -> String {
let mut s = String::new();
for span in &self.spans {
s.push_str(&span.text);
}
s
}
fn span_index_for_offset(offsets: &[usize], total_len: usize, offset: usize) -> usize {
let mut lo = 0usize;
let mut hi = offsets.len();
while lo < hi {
let mid = (lo + hi) / 2;
if offsets[mid] <= offset {
lo = mid + 1;
} else {
hi = mid;
}
}
let idx = lo.saturating_sub(1);
idx.min(offsets.len().saturating_sub(1))
.min(if total_len == 0 { 0 } else { offsets.len() - 1 })
}
}
impl Widget for Spangroup {
fn bounds(&self) -> Rect {
self.bounds
}
fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
Some(&mut self.font)
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
fn draw(&self, renderer: &mut dyn Renderer) {
draw_widget_bg(renderer, self.bounds, &self.style);
if self.spans.is_empty() || self.bounds.width <= 0 || self.bounds.height <= 0 {
return;
}
let font = self.font.resolve();
let metrics = font.line_metrics();
let (concat, offsets) = self.concat_with_offsets();
if concat.is_empty() {
return;
}
let total_len = concat.len();
let wrapped = wrap_greedy_ltr(font, &concat, self.bounds.width.max(1), 0, 0);
let mut clipped = ClipRenderer::new(renderer, self.bounds);
let mut line_y = self.bounds.y + metrics.ascent as i32;
for line in &wrapped.lines {
if self.overflow == SpanOverflow::Clip
&& line_y - metrics.ascent as i32 >= self.bounds.y + self.bounds.height
{
break;
}
let mut pos = line.start;
let line_end = line.end;
let mut x_cursor = self.bounds.x;
while pos < line_end {
let span_idx = Self::span_index_for_offset(&offsets, total_len, pos);
let span = &self.spans[span_idx];
let span_end_in_concat = if span_idx + 1 < offsets.len() {
offsets[span_idx + 1]
} else {
total_len
};
let piece_end = line_end.min(span_end_in_concat);
let piece = &concat[pos..piece_end];
if !piece.is_empty() {
let shaped = shape_text_ltr(font, piece, (x_cursor, line_y), 0);
let color = span.style.color.with_alpha(self.style.alpha);
clipped.draw_text_shaped(&shaped, (0, 0), color);
x_cursor += (shaped.total_advance_fp16 + 8) >> 4;
}
pos = piece_end;
}
line_y += metrics.line_height as i32;
}
}
fn handle_event(&mut self, _event: &Event) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use rlvgl_core::bitmap_font::FONT_6X10;
fn r(x: i32, y: i32, w: i32, h: i32) -> Rect {
Rect {
x,
y,
width: w,
height: h,
}
}
fn red() -> SpanStyle {
SpanStyle::with_color(Color(255, 0, 0, 255))
}
fn blue() -> SpanStyle {
SpanStyle::with_color(Color(0, 0, 255, 255))
}
#[test]
fn add_and_count_spans() {
let mut sg = Spangroup::new(r(0, 0, 200, 100));
let a = sg.add_span("hello", red());
let b = sg.add_span(" world", blue());
assert_eq!(sg.span_count(), 2);
assert_ne!(a, SPAN_NONE);
assert_ne!(b, SPAN_NONE);
assert_ne!(a, b);
}
#[test]
fn remove_span() {
let mut sg = Spangroup::new(r(0, 0, 200, 100));
let a = sg.add_span("foo", red());
sg.add_span("bar", blue());
sg.remove_span(a);
assert_eq!(sg.span_count(), 1);
}
#[test]
fn clear_spans() {
let mut sg = Spangroup::new(r(0, 0, 200, 100));
sg.add_span("a", red());
sg.add_span("b", blue());
sg.clear_spans();
assert_eq!(sg.span_count(), 0);
}
#[test]
fn set_span_text_and_style() {
let mut sg = Spangroup::new(r(0, 0, 200, 100));
let id = sg.add_span("initial", red());
sg.set_span_text(id, "updated");
assert_eq!(sg.spans[0].text, "updated");
sg.set_span_style(id, blue());
assert_eq!(sg.spans[0].style, blue());
}
#[test]
fn wrap_uses_shared_measurement_not_per_span_arithmetic() {
let font: &dyn FontMetrics = &FONT_6X10;
let mut sg = Spangroup::new(r(0, 0, 1, 200)); sg.add_span("ab", red());
sg.add_span("cd", blue());
let concat = sg.concat_text();
let wrapped = wrap_greedy_ltr(font, &concat, 1, 0, 0);
assert!(
wrapped.lines.len() > 1,
"expected wrap across span boundary; got {} lines",
wrapped.lines.len()
);
}
#[test]
fn span_index_for_offset_basic() {
let offsets = vec![0usize, 5, 10];
let total = 15;
assert_eq!(Spangroup::span_index_for_offset(&offsets, total, 0), 0);
assert_eq!(Spangroup::span_index_for_offset(&offsets, total, 4), 0);
assert_eq!(Spangroup::span_index_for_offset(&offsets, total, 5), 1);
assert_eq!(Spangroup::span_index_for_offset(&offsets, total, 9), 1);
assert_eq!(Spangroup::span_index_for_offset(&offsets, total, 10), 2);
assert_eq!(Spangroup::span_index_for_offset(&offsets, total, 14), 2);
}
#[test]
fn content_height_non_zero_for_non_empty_spans() {
let mut sg = Spangroup::new(r(0, 0, 100, 100));
sg.add_span("Some text here", red());
assert!(sg.content_height() > 0);
}
#[test]
fn set_bounds_updates_geometry() {
let mut sg = Spangroup::new(r(0, 0, 100, 100));
sg.set_bounds(r(10, 20, 200, 150));
assert_eq!(sg.bounds(), r(10, 20, 200, 150));
}
#[test]
fn overflow_clip_vs_expand() {
let mut sg = Spangroup::new(r(0, 0, 100, 100));
assert_eq!(sg.overflow(), SpanOverflow::Clip);
sg.set_overflow(SpanOverflow::Expand);
assert_eq!(sg.overflow(), SpanOverflow::Expand);
}
}