use std::ops::{Deref, DerefMut};
use blinc_core::Color;
use blinc_theme::{ColorToken, ThemeState};
use crate::div::{div, Div, ElementBuilder};
use crate::element::RenderProps;
use crate::svg::svg;
use crate::text::text;
use crate::tree::{LayoutNodeId, LayoutTree};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ListMarker {
#[default]
Disc,
Circle,
Square,
Decimal,
LowerAlpha,
UpperAlpha,
LowerRoman,
UpperRoman,
None,
}
impl ListMarker {
pub fn marker_for(&self, index: usize) -> String {
match self {
ListMarker::Disc => "•".to_string(),
ListMarker::Circle => "○".to_string(),
ListMarker::Square => "▪".to_string(),
ListMarker::Decimal => format!("{}.", index + 1),
ListMarker::LowerAlpha => {
if index < 26 {
format!("{}.", (b'a' + index as u8) as char)
} else {
format!("{}.", index + 1)
}
}
ListMarker::UpperAlpha => {
if index < 26 {
format!("{}.", (b'A' + index as u8) as char)
} else {
format!("{}.", index + 1)
}
}
ListMarker::LowerRoman => format!("{}.", to_roman(index + 1).to_lowercase()),
ListMarker::UpperRoman => format!("{}.", to_roman(index + 1)),
ListMarker::None => String::new(),
}
}
}
fn to_roman(mut n: usize) -> String {
if n == 0 || n > 3999 {
return n.to_string();
}
let numerals = [
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
let mut result = String::new();
for (value, symbol) in numerals {
while n >= value {
result.push_str(symbol);
n -= value;
}
}
result
}
#[derive(Clone, Debug)]
pub struct ListConfig {
pub marker_color: Color,
pub marker_width: f32,
pub marker_gap: f32,
pub item_spacing: f32,
pub indent: f32,
pub marker_font_size: f32,
}
impl Default for ListConfig {
fn default() -> Self {
let theme = ThemeState::get();
Self {
marker_color: theme.color(ColorToken::TextTertiary),
marker_width: 24.0,
marker_gap: 8.0,
item_spacing: 4.0,
indent: 0.0,
marker_font_size: 14.0,
}
}
}
pub struct UnorderedList {
inner: Div,
config: ListConfig,
marker: ListMarker,
item_count: usize,
css_element_id: Option<String>,
css_classes: Vec<String>,
}
impl UnorderedList {
pub fn new() -> Self {
Self::with_config(ListConfig::default())
}
pub fn with_config(config: ListConfig) -> Self {
let inner = div().flex_col().gap(config.item_spacing).ml(config.indent);
Self {
inner,
config,
marker: ListMarker::Disc,
item_count: 0,
css_element_id: None,
css_classes: Vec::new(),
}
}
pub fn child(mut self, item: ListItem) -> Self {
let item = item.with_marker_and_config(self.marker, Some(self.item_count), &self.config);
self.inner = self.inner.child(item);
self.item_count += 1;
self
}
pub fn child_element(mut self, element: impl ElementBuilder + 'static) -> Self {
self.inner = self.inner.child(element);
self
}
pub fn marker(mut self, marker: ListMarker) -> Self {
self.marker = marker;
self
}
pub fn indent(mut self, indent: f32) -> Self {
self.config.indent = indent;
self.inner = self.inner.ml(indent);
self
}
pub fn spacing(mut self, spacing: f32) -> Self {
self.config.item_spacing = spacing;
self.inner = self.inner.gap(spacing);
self
}
pub fn id(mut self, id: &str) -> Self {
self.css_element_id = Some(id.to_string());
self
}
pub fn class(mut self, name: &str) -> Self {
self.css_classes.push(name.to_string());
self
}
}
impl Default for UnorderedList {
fn default() -> Self {
Self::new()
}
}
impl Deref for UnorderedList {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for UnorderedList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl ElementBuilder for UnorderedList {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> crate::div::ElementTypeId {
self.inner.element_type_id()
}
fn semantic_type_name(&self) -> Option<&'static str> {
Some("ul")
}
fn element_id(&self) -> Option<&str> {
self.css_element_id.as_deref()
}
fn element_classes(&self) -> &[String] {
&self.css_classes
}
}
pub struct OrderedList {
inner: Div,
config: ListConfig,
marker: ListMarker,
start: usize,
item_count: usize,
css_element_id: Option<String>,
css_classes: Vec<String>,
}
impl OrderedList {
pub fn new() -> Self {
Self::starting_at(1)
}
pub fn with_config(config: ListConfig) -> Self {
Self::starting_at_with_config(1, config)
}
pub fn starting_at(start: usize) -> Self {
Self::starting_at_with_config(start, ListConfig::default())
}
pub fn starting_at_with_config(start: usize, config: ListConfig) -> Self {
let inner = div().flex_col().gap(config.item_spacing).ml(config.indent);
Self {
inner,
config,
marker: ListMarker::Decimal,
start,
item_count: 0,
css_element_id: None,
css_classes: Vec::new(),
}
}
pub fn child(mut self, item: ListItem) -> Self {
let item = item.with_marker_and_config(
self.marker,
Some(self.start + self.item_count - 1),
&self.config,
);
self.inner = self.inner.child(item);
self.item_count += 1;
self
}
pub fn child_element(mut self, element: impl ElementBuilder + 'static) -> Self {
self.inner = self.inner.child(element);
self
}
pub fn marker(mut self, marker: ListMarker) -> Self {
self.marker = marker;
self
}
pub fn start(mut self, start: usize) -> Self {
self.start = start;
self
}
pub fn indent(mut self, indent: f32) -> Self {
self.config.indent = indent;
self.inner = self.inner.ml(indent);
self
}
pub fn spacing(mut self, spacing: f32) -> Self {
self.config.item_spacing = spacing;
self.inner = self.inner.gap(spacing);
self
}
pub fn id(mut self, id: &str) -> Self {
self.css_element_id = Some(id.to_string());
self
}
pub fn class(mut self, name: &str) -> Self {
self.css_classes.push(name.to_string());
self
}
}
impl Default for OrderedList {
fn default() -> Self {
Self::new()
}
}
impl Deref for OrderedList {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for OrderedList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl ElementBuilder for OrderedList {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> crate::div::ElementTypeId {
self.inner.element_type_id()
}
fn semantic_type_name(&self) -> Option<&'static str> {
Some("ol")
}
fn element_id(&self) -> Option<&str> {
self.css_element_id.as_deref()
}
fn element_classes(&self) -> &[String] {
&self.css_classes
}
}
pub struct ListItem {
inner: Div,
content: Div,
marker: Option<ListMarker>,
index: Option<usize>,
config: ListConfig,
}
impl ListItem {
pub fn new() -> Self {
let config = ListConfig::default();
let inner = div().flex_row().items_start().gap(config.marker_gap);
let content = div().flex_col().flex_1().gap(4.0);
Self {
inner,
content,
marker: None,
index: None,
config,
}
}
pub fn child(mut self, child: impl ElementBuilder + 'static) -> Self {
self.content = self.content.child(child);
self
}
pub fn child_box(mut self, child: Box<dyn crate::div::ElementBuilder>) -> Self {
self.content = self.content.child_box(child);
self
}
fn with_marker(self, marker: ListMarker, index: Option<usize>) -> Self {
self.with_marker_and_config(marker, index, &ListConfig::default())
}
fn with_marker_and_config(
mut self,
marker: ListMarker,
index: Option<usize>,
config: &ListConfig,
) -> Self {
self.marker = Some(marker);
self.index = index;
let marker_str = marker.marker_for(index.unwrap_or(0));
let marker_element = text(&marker_str)
.size(config.marker_font_size)
.color(config.marker_color);
let marker_div = div()
.w(config.marker_width)
.flex_shrink_0()
.child(marker_element);
self.inner = div()
.flex_row()
.items_start()
.gap(config.marker_gap)
.child(marker_div)
.child(std::mem::replace(&mut self.content, div()));
self
}
}
impl Default for ListItem {
fn default() -> Self {
Self::new()
}
}
impl Deref for ListItem {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for ListItem {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl ElementBuilder for ListItem {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> crate::div::ElementTypeId {
self.inner.element_type_id()
}
fn semantic_type_name(&self) -> Option<&'static str> {
Some("li")
}
fn element_id(&self) -> Option<&str> {
self.inner.element_id()
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
pub struct TaskListItem {
inner: Div,
checked: bool,
config: ListConfig,
}
impl TaskListItem {
pub fn new(checked: bool) -> Self {
Self::with_config(checked, ListConfig::default())
}
pub fn with_config(checked: bool, config: ListConfig) -> Self {
let checkbox_size = config.marker_font_size;
let border_width = 1.5;
let checkbox_box = if checked {
let checkmark_svg = r#"<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8L6.5 11.5L13 4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>"#;
div()
.w(checkbox_size)
.h(checkbox_size)
.flex_shrink_0()
.bg(config.marker_color)
.rounded(2.0)
.items_center()
.justify_center()
.child(
svg(checkmark_svg)
.size(checkbox_size - 4.0, checkbox_size - 4.0)
.tint(Color::WHITE),
)
} else {
div()
.w(checkbox_size)
.h(checkbox_size)
.flex_shrink_0()
.rounded(2.0)
.border(border_width, config.marker_color)
};
let checkbox_container = div()
.w(config.marker_width)
.flex_shrink_0()
.items_center()
.justify_center()
.child(checkbox_box);
let inner = div()
.flex_row()
.items_start()
.gap(config.marker_gap)
.child(checkbox_container);
Self {
inner,
checked,
config,
}
}
pub fn child(mut self, child: impl ElementBuilder + 'static) -> Self {
self.inner = self.inner.child(child);
self
}
pub fn child_box(mut self, child: Box<dyn crate::div::ElementBuilder>) -> Self {
self.inner = self.inner.child_box(child);
self
}
pub fn is_checked(&self) -> bool {
self.checked
}
}
impl Default for TaskListItem {
fn default() -> Self {
Self::new(false)
}
}
impl Deref for TaskListItem {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for TaskListItem {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl ElementBuilder for TaskListItem {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> crate::div::ElementTypeId {
self.inner.element_type_id()
}
fn semantic_type_name(&self) -> Option<&'static str> {
Some("li")
}
fn element_id(&self) -> Option<&str> {
self.inner.element_id()
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
pub fn ul() -> UnorderedList {
UnorderedList::new()
}
pub fn ul_with_config(config: ListConfig) -> UnorderedList {
UnorderedList::with_config(config)
}
pub fn ol() -> OrderedList {
OrderedList::new()
}
pub fn ol_with_config(config: ListConfig) -> OrderedList {
OrderedList::with_config(config)
}
pub fn ol_start(start: usize) -> OrderedList {
OrderedList::starting_at(start)
}
pub fn ol_start_with_config(start: usize, config: ListConfig) -> OrderedList {
OrderedList::starting_at_with_config(start, config)
}
pub fn li() -> ListItem {
ListItem::new()
}
pub fn task_item(checked: bool) -> TaskListItem {
TaskListItem::new(checked)
}
pub fn task_item_with_config(checked: bool, config: ListConfig) -> TaskListItem {
TaskListItem::with_config(checked, config)
}
#[cfg(test)]
mod tests {
use super::*;
fn init_theme() {
let _ = ThemeState::try_get().unwrap_or_else(|| {
ThemeState::init_default();
ThemeState::get()
});
}
#[test]
fn test_unordered_list() {
init_theme();
let mut tree = LayoutTree::new();
let list = ul().child(li().child(div())).child(li().child(div()));
list.build(&mut tree);
assert!(!tree.is_empty());
}
#[test]
fn test_ordered_list() {
init_theme();
let mut tree = LayoutTree::new();
let list = ol().child(li().child(div())).child(li().child(div()));
list.build(&mut tree);
assert!(!tree.is_empty());
}
#[test]
fn test_task_list() {
init_theme();
let mut tree = LayoutTree::new();
let item = task_item(true).child(div());
item.build(&mut tree);
assert!(!tree.is_empty());
}
#[test]
fn test_roman_numerals() {
assert_eq!(to_roman(1), "I");
assert_eq!(to_roman(4), "IV");
assert_eq!(to_roman(9), "IX");
assert_eq!(to_roman(42), "XLII");
assert_eq!(to_roman(99), "XCIX");
}
#[test]
fn test_markers() {
assert_eq!(ListMarker::Disc.marker_for(0), "•");
assert_eq!(ListMarker::Decimal.marker_for(0), "1.");
assert_eq!(ListMarker::Decimal.marker_for(9), "10.");
assert_eq!(ListMarker::LowerAlpha.marker_for(0), "a.");
assert_eq!(ListMarker::LowerAlpha.marker_for(25), "z.");
}
}