use std::panic::Location;
use crate::text::metrics::line_width;
use crate::tokens;
use crate::tree::*;
use crate::widgets::text::text;
const BULLET_GLYPH: &str = "\u{2022}";
const MARKER_GAP: f32 = tokens::SPACE_2;
const ITEM_GAP: f32 = tokens::SPACE_1;
#[track_caller]
pub fn bullet_list<I, E>(items: I) -> El
where
I: IntoIterator<Item = E>,
E: Into<El>,
{
let loc = Location::caller();
let marker_slot_width = bullet_marker_width();
let item_els: Vec<El> = items
.into_iter()
.map(|item| {
let marker = text(BULLET_GLYPH)
.at_loc(loc)
.text_color(tokens::MUTED_FOREGROUND)
.center_text()
.width(Size::Fixed(marker_slot_width));
list_item(marker, marker_slot_width, item.into(), loc)
})
.collect();
column(item_els)
.at_loc(loc)
.width(Size::Fill(1.0))
.height(Size::Hug)
.default_gap(ITEM_GAP)
}
#[track_caller]
pub fn numbered_list<I, E>(items: I) -> El
where
I: IntoIterator<Item = E>,
E: Into<El>,
{
let loc = Location::caller();
let items_vec: Vec<El> = items.into_iter().map(Into::into).collect();
let marker_slot_width = numbered_marker_width(items_vec.len());
let item_els: Vec<El> = items_vec
.into_iter()
.enumerate()
.map(|(i, item)| {
let marker = text(format!("{}.", i + 1))
.at_loc(loc)
.text_color(tokens::MUTED_FOREGROUND)
.end_text()
.width(Size::Fixed(marker_slot_width));
list_item(marker, marker_slot_width, item, loc)
})
.collect();
column(item_els)
.at_loc(loc)
.width(Size::Fill(1.0))
.height(Size::Hug)
.default_gap(ITEM_GAP)
}
fn list_item(
marker: El,
marker_slot_width: f32,
content: El,
loc: &'static std::panic::Location<'static>,
) -> El {
let content_indent = marker_slot_width + MARKER_GAP;
let body = column([normalize_item_content(content)])
.at_loc(loc)
.width(Size::Fill(1.0))
.height(Size::Hug)
.default_padding(Sides {
left: content_indent,
right: 0.0,
top: 0.0,
bottom: 0.0,
});
let marker_slot = column([marker])
.at_loc(loc)
.width(Size::Fixed(marker_slot_width))
.height(Size::Hug);
stack([marker_slot, body])
.at_loc(loc)
.width(Size::Fill(1.0))
.height(Size::Hug)
}
fn normalize_item_content(content: El) -> El {
if matches!(content.kind, Kind::Text) {
return content.wrap_text().width(Size::Fill(1.0));
}
content
}
fn bullet_marker_width() -> f32 {
let glyph_w = line_width(
BULLET_GLYPH,
tokens::TEXT_BASE.size,
FontWeight::Regular,
false,
);
(glyph_w + 4.0).ceil()
}
fn numbered_marker_width(count: usize) -> f32 {
let widest_num = count.max(1);
let sample = format!("{}.", widest_num);
let w = line_width(&sample, tokens::TEXT_BASE.size, FontWeight::Regular, false);
(w + 2.0).ceil()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bullet_list_overlays_marker_slot_and_content_per_item() {
let l = bullet_list(["one", "two", "three"]);
assert_eq!(l.kind, Kind::Group);
assert_eq!(l.axis, Axis::Column);
assert_eq!(l.width, Size::Fill(1.0));
assert_eq!(l.children.len(), 3);
for item in &l.children {
assert_eq!(item.kind, Kind::Group);
assert_eq!(item.axis, Axis::Overlay);
assert_eq!(item.children.len(), 2);
let marker_slot = &item.children[0];
assert!(matches!(marker_slot.width, Size::Fixed(_)));
let marker = &marker_slot.children[0];
assert_eq!(marker.text.as_deref(), Some(BULLET_GLYPH));
assert_eq!(marker.text_color, Some(tokens::MUTED_FOREGROUND));
let body = &item.children[1];
assert_eq!(body.kind, Kind::Group);
assert_eq!(body.axis, Axis::Column);
assert_eq!(body.width, Size::Fill(1.0));
assert!(body.padding.left > MARKER_GAP);
assert_eq!(body.children.len(), 1);
}
}
#[test]
fn numbered_list_markers_count_from_one_and_right_align() {
let l = numbered_list(["alpha", "beta", "gamma"]);
let labels: Vec<&str> = l
.children
.iter()
.map(|item| {
let marker_slot = &item.children[0];
marker_slot.children[0].text.as_deref().unwrap_or("")
})
.collect();
assert_eq!(labels, vec!["1.", "2.", "3."]);
for item in &l.children {
let marker_slot = &item.children[0];
let marker = &marker_slot.children[0];
assert_eq!(marker.text_align, TextAlign::End);
}
}
#[test]
fn numbered_marker_width_grows_with_count() {
let small = numbered_marker_width(9);
let large = numbered_marker_width(99);
let huge = numbered_marker_width(999);
assert!(large > small, "{large} <= {small}");
assert!(huge > large, "{huge} <= {large}");
}
#[test]
fn plain_text_items_are_wrapped_inside_the_content_column() {
let l = bullet_list(["This item is plain text and should wrap to fit."]);
let body = &l.children[0].children[1];
let inner = &body.children[0];
assert_eq!(inner.kind, Kind::Text);
assert_eq!(inner.text_wrap, TextWrap::Wrap);
assert_eq!(inner.width, Size::Fill(1.0));
}
#[test]
fn composite_items_pass_through_unchanged() {
let l = bullet_list(vec![text_runs([text("rich"), text(" runs")])]);
let body = &l.children[0].children[1];
let inner = &body.children[0];
assert_eq!(inner.kind, Kind::Inlines);
}
}