use smallvec::SmallVec;
use crate::class_item::ClassItem;
use crate::class_item::single::Single;
use crate::class_item::toggle::Toggle;
use crate::class_name::ClassName;
use crate::classes::MergeStrategy;
use crate::condition::ClassCondition;
#[derive(Clone, Debug)]
pub(crate) struct ClassList {
items: SmallVec<[ClassItem; 3]>,
is_reactive: bool,
estimated_class_len: usize,
}
impl Default for ClassList {
fn default() -> Self {
Self::empty()
}
}
impl ClassList {
pub(crate) fn empty() -> Self {
Self {
items: SmallVec::new(),
is_reactive: false,
estimated_class_len: 0,
}
}
pub(crate) fn add_single(&mut self, name: ClassName, when: ClassCondition) {
self.check_unique(&name);
self.push_item(ClassItem::Single(Single { name, when }));
}
pub(crate) fn add_toggle(
&mut self,
when: ClassCondition,
when_true: ClassName,
when_false: ClassName,
) {
if when_true == when_false {
panic_toggle_branches_equal(&when_false);
}
self.check_unique(&when_true);
self.check_unique(&when_false);
self.push_item(ClassItem::Toggle(Toggle {
when,
when_true,
when_false,
}));
}
pub(crate) fn merge(&mut self, other: ClassList, strategy: MergeStrategy) {
if other.items.is_empty() {
return;
}
self.items.reserve(other.items.len().saturating_mul(2));
for incoming in other.items {
self.merge_one(incoming, strategy);
}
self.recompute_caches();
}
fn merge_one(&mut self, incoming: ClassItem, strategy: MergeStrategy) {
match incoming {
ClassItem::Single(single) => self.merge_incoming_single(single, strategy),
ClassItem::Toggle(toggle) => {
let collides = self.items.iter().any(|item| {
item.has_name(&toggle.when_true) || item.has_name(&toggle.when_false)
});
if collides {
let (s_true, s_false) = toggle.into_singles();
self.merge_incoming_single(s_true, strategy);
self.merge_incoming_single(s_false, strategy);
} else {
self.items.push(ClassItem::Toggle(toggle));
}
}
}
}
fn merge_incoming_single(&mut self, incoming: Single, strategy: MergeStrategy) {
match self.find_item_index(&incoming.name) {
None => self.items.push(ClassItem::Single(incoming)),
Some(idx) => match strategy {
MergeStrategy::PanicOnConflict => panic_duplicate(&incoming.name),
MergeStrategy::KeepSelf => {}
MergeStrategy::UnionConditions => self.union_at(idx, incoming),
},
}
}
fn union_at(&mut self, idx: usize, incoming: Single) {
match &mut self.items[idx] {
ClassItem::Single(existing) => {
let prev = std::mem::replace(&mut existing.when, ClassCondition::never());
existing.when = prev.or(incoming.when);
}
ClassItem::Toggle(_) => {
let ClassItem::Toggle(toggle) = self.items.remove(idx) else {
unreachable!("matched on Toggle immediately above")
};
let (s_true, s_false) = toggle.into_singles();
let (when_true_single, when_false_single) = if s_true.name == incoming.name {
let merged = Single {
name: s_true.name,
when: s_true.when.or(incoming.when),
};
(merged, s_false)
} else {
let merged = Single {
name: s_false.name,
when: s_false.when.or(incoming.when),
};
(s_true, merged)
};
self.items.insert(idx, ClassItem::Single(when_true_single));
self.items
.insert(idx + 1, ClassItem::Single(when_false_single));
}
}
}
fn find_item_index(&self, name: &ClassName) -> Option<usize> {
self.items.iter().position(|item| item.has_name(name))
}
fn check_unique(&self, name: &ClassName) {
if self.find_item_index(name).is_some() {
panic_duplicate(name);
}
}
fn push_item(&mut self, item: ClassItem) {
let separator_cost = usize::from(!self.items.is_empty());
self.estimated_class_len += item.rendered_byte_estimate() + separator_cost;
if item.is_reactive() {
self.is_reactive = true;
}
self.items.push(item);
}
fn recompute_caches(&mut self) {
let mut is_reactive = false;
let mut estimated_class_len = 0;
for (i, item) in self.items.iter().enumerate() {
if item.is_reactive() {
is_reactive = true;
}
estimated_class_len += item.rendered_byte_estimate() + usize::from(i > 0);
}
self.is_reactive = is_reactive;
self.estimated_class_len = estimated_class_len;
}
pub(crate) fn write_active_classes(&self, buf: &mut String) {
let mut first = buf.is_empty();
for item in &self.items {
item.append_if_active(buf, &mut first);
}
}
pub(crate) fn estimated_class_len(&self) -> usize {
self.estimated_class_len
}
pub(crate) fn is_reactive(&self) -> bool {
self.is_reactive
}
pub(crate) fn touch_reactive_dependencies(&self) {
for item in &self.items {
item.touch_reactive_dependencies();
}
}
}
fn panic_duplicate(name: &ClassName) -> ! {
let name = name.as_str();
panic!(
"class token `{name}` was registered with Classes more than once. \
Each class name may appear in at most one entry; \
combine conditions instead \
(e.g. add_reactive({name:?}, move || a.get() || b.get()))."
);
}
fn panic_toggle_branches_equal(name: &ClassName) -> ! {
let name = name.as_str();
panic!(
"add_toggle requires two distinct branch names, but both branches were `{name}`. \
A toggle whose true/false branches share a name has no effect; \
use Classes::add (or Classes::add_reactive) instead."
);
}
#[cfg(test)]
mod tests {
use assertr::prelude::*;
use leptos::prelude::{Set, signal};
use crate::class_name::ClassName;
use crate::condition::ClassCondition;
use super::ClassList;
#[test]
fn add_single_distinct_names_render_in_order() {
let mut list = ClassList::empty();
list.add_single(ClassName::from("foo"), ClassCondition::always());
list.add_single(ClassName::from("bar"), ClassCondition::always());
let mut buf = String::new();
list.write_active_classes(&mut buf);
assert_that!(buf).is_equal_to("foo bar".to_string());
}
#[test]
#[should_panic(expected = "was registered with Classes more than once")]
fn add_single_duplicate_name_panics() {
let mut list = ClassList::empty();
list.add_single(ClassName::from("foo"), ClassCondition::always());
list.add_single(ClassName::from("foo"), ClassCondition::always());
}
#[test]
#[should_panic(expected = "was registered with Classes more than once")]
fn add_single_duplicate_across_active_and_inactive_panics() {
let mut list = ClassList::empty();
list.add_single(ClassName::from("foo"), ClassCondition::always());
list.add_single(ClassName::from("foo"), ClassCondition::never());
}
#[test]
#[should_panic(expected = "was registered with Classes more than once")]
fn add_toggle_collides_with_existing_single_panics() {
let mut list = ClassList::empty();
list.add_single(ClassName::from("foo"), ClassCondition::always());
list.add_toggle(
ClassCondition::always(),
ClassName::from("foo"),
ClassName::from("bar"),
);
}
#[test]
#[should_panic(expected = "add_toggle requires two distinct branch names")]
fn add_toggle_with_identical_branches_panics() {
let mut list = ClassList::empty();
list.add_toggle(
ClassCondition::always(),
ClassName::from("foo"),
ClassName::from("foo"),
);
}
#[test]
#[should_panic(expected = "was registered with Classes more than once")]
fn add_single_collides_with_toggle_false_branch_panics() {
let mut list = ClassList::empty();
list.add_toggle(
ClassCondition::always(),
ClassName::from("a"),
ClassName::from("b"),
);
list.add_single(ClassName::from("b"), ClassCondition::always());
}
#[test]
fn add_toggle_renders_true_branch_when_active() {
let mut list = ClassList::empty();
list.add_toggle(
ClassCondition::always(),
ClassName::from("on"),
ClassName::from("off"),
);
let mut buf = String::new();
list.write_active_classes(&mut buf);
assert_that!(buf).is_equal_to("on".to_string());
}
#[test]
fn add_toggle_renders_false_branch_when_inactive() {
let mut list = ClassList::empty();
list.add_toggle(
ClassCondition::never(),
ClassName::from("on"),
ClassName::from("off"),
);
let mut buf = String::new();
list.write_active_classes(&mut buf);
assert_that!(buf).is_equal_to("off".to_string());
}
#[test]
fn add_toggle_with_signal_flips_branch_reactively() {
let (active, set_active) = signal(true);
let mut list = ClassList::empty();
list.add_toggle(
ClassCondition::when_signal(active),
ClassName::from("on"),
ClassName::from("off"),
);
assert_that!(list.is_reactive()).is_true();
let mut buf = String::new();
list.write_active_classes(&mut buf);
assert_that!(buf).is_equal_to("on".to_string());
set_active.set(false);
let mut buf = String::new();
list.write_active_classes(&mut buf);
assert_that!(buf).is_equal_to("off".to_string());
}
#[test]
fn estimated_len_for_toggle_uses_longer_branch() {
let mut list = ClassList::empty();
list.add_toggle(
ClassCondition::always(),
ClassName::from("active-state"),
ClassName::from("off"),
);
assert_that!(list.estimated_class_len()).is_equal_to(12);
}
#[test]
fn estimated_len_sums_with_separator_after_first_entry() {
let mut list = ClassList::empty();
list.add_single(ClassName::from("base"), ClassCondition::always());
list.add_single(ClassName::from("tail"), ClassCondition::always());
assert_that!(list.estimated_class_len()).is_equal_to(9);
}
mod merge {
use assertr::prelude::*;
use leptos::prelude::{Set, signal};
use crate::class_list::ClassList;
use crate::class_name::ClassName;
use crate::classes::MergeStrategy;
use crate::condition::ClassCondition;
pub(super) fn list_with(entries: &[(&'static str, ClassCondition)]) -> ClassList {
let mut list = ClassList::empty();
for (name, when) in entries {
list.add_single(ClassName::from(*name), when.clone());
}
list
}
pub(super) fn rendered(list: &ClassList) -> String {
let mut buf = String::new();
list.write_active_classes(&mut buf);
buf
}
mod panic_on_conflict {
use super::*;
#[test]
fn non_overlapping_appends_in_order() {
let mut a = list_with(&[("foo", ClassCondition::always())]);
let b = list_with(&[("bar", ClassCondition::always())]);
a.merge(b, MergeStrategy::PanicOnConflict);
assert_that!(rendered(&a)).is_equal_to("foo bar".to_string());
}
#[test]
fn empty_other_is_identity() {
let mut a = list_with(&[("foo", ClassCondition::always())]);
a.merge(ClassList::empty(), MergeStrategy::PanicOnConflict);
assert_that!(rendered(&a)).is_equal_to("foo".to_string());
}
#[test]
fn empty_self_yields_other() {
let mut a = ClassList::empty();
let b = list_with(&[("foo", ClassCondition::always())]);
a.merge(b, MergeStrategy::PanicOnConflict);
assert_that!(rendered(&a)).is_equal_to("foo".to_string());
}
#[test]
#[should_panic(expected = "was registered with Classes more than once")]
fn panics_on_single_collision() {
let mut a = list_with(&[("foo", ClassCondition::always())]);
let b = list_with(&[("foo", ClassCondition::always())]);
a.merge(b, MergeStrategy::PanicOnConflict);
}
#[test]
#[should_panic(expected = "was registered with Classes more than once")]
fn panics_on_toggle_half_collision() {
let mut a = ClassList::empty();
a.add_toggle(
ClassCondition::always(),
ClassName::from("on"),
ClassName::from("off"),
);
let b = list_with(&[("on", ClassCondition::always())]);
a.merge(b, MergeStrategy::PanicOnConflict);
}
#[test]
fn recomputed_caches_match_manual_construction() {
let (s, _) = signal(true);
let mut a = list_with(&[("foo", ClassCondition::always())]);
let b = list_with(&[("bar", ClassCondition::when_signal(s))]);
a.merge(b, MergeStrategy::PanicOnConflict);
assert_that!(a.estimated_class_len()).is_equal_to(7);
assert_that!(a.is_reactive()).is_true();
}
#[test]
fn non_colliding_toggle_from_other_is_preserved_structurally() {
let mut a = list_with(&[("foo", ClassCondition::always())]);
let mut b = ClassList::empty();
b.add_toggle(
ClassCondition::always(),
ClassName::from("active-state"),
ClassName::from("x"),
);
a.merge(b, MergeStrategy::PanicOnConflict);
assert_that!(a.estimated_class_len()).is_equal_to(16);
assert_that!(rendered(&a)).is_equal_to("foo active-state".to_string());
}
}
mod keep_self {
use super::*;
#[test]
fn drops_other_entry_on_collision() {
let (s, _) = signal(false);
let mut a = list_with(&[("foo", ClassCondition::always())]);
let b = list_with(&[
("foo", ClassCondition::when_signal(s)),
("bar", ClassCondition::always()),
]);
a.merge(b, MergeStrategy::KeepSelf);
assert_that!(rendered(&a)).is_equal_to("foo bar".to_string());
assert_that!(a.is_reactive()).is_false();
}
#[test]
fn preserves_self_toggle_when_other_collides() {
let (s, set_s) = signal(true);
let mut a = ClassList::empty();
a.add_toggle(
ClassCondition::when_signal(s),
ClassName::from("on"),
ClassName::from("off"),
);
let b = list_with(&[("on", ClassCondition::always())]);
a.merge(b, MergeStrategy::KeepSelf);
assert_that!(rendered(&a)).is_equal_to("on".to_string());
set_s.set(false);
assert_that!(rendered(&a)).is_equal_to("off".to_string());
}
#[test]
fn lets_other_toggle_orphan_survive_as_flat_entry() {
let (s, set_s) = signal(true);
let mut a = list_with(&[("on", ClassCondition::always())]);
let mut b = ClassList::empty();
b.add_toggle(
ClassCondition::when_signal(s),
ClassName::from("on"),
ClassName::from("off"),
);
a.merge(b, MergeStrategy::KeepSelf);
assert_that!(rendered(&a)).is_equal_to("on".to_string());
set_s.set(false);
assert_that!(rendered(&a)).is_equal_to("on off".to_string());
}
#[test]
fn other_toggle_false_branch_collision_orphans_true_branch() {
let (s, set_s) = signal(false);
let mut a = list_with(&[("off", ClassCondition::always())]);
let mut b = ClassList::empty();
b.add_toggle(
ClassCondition::when_signal(s),
ClassName::from("on"),
ClassName::from("off"),
);
a.merge(b, MergeStrategy::KeepSelf);
assert_that!(rendered(&a)).is_equal_to("off".to_string());
set_s.set(true);
assert_that!(rendered(&a)).is_equal_to("off on".to_string());
}
#[test]
fn preserves_self_toggle_when_other_collides_on_false_branch() {
let (s, set_s) = signal(true);
let mut a = ClassList::empty();
a.add_toggle(
ClassCondition::when_signal(s),
ClassName::from("on"),
ClassName::from("off"),
);
let b = list_with(&[("off", ClassCondition::always())]);
a.merge(b, MergeStrategy::KeepSelf);
assert_that!(rendered(&a)).is_equal_to("on".to_string());
set_s.set(false);
assert_that!(rendered(&a)).is_equal_to("off".to_string());
}
}
mod union_conditions {
use super::*;
#[test]
fn always_collapses_to_always() {
let (s, _) = signal(false);
let mut a = list_with(&[("foo", ClassCondition::always())]);
let b = list_with(&[("foo", ClassCondition::when_signal(s))]);
a.merge(b, MergeStrategy::UnionConditions);
assert_that!(rendered(&a)).is_equal_to("foo".to_string());
assert_that!(a.is_reactive()).is_false();
}
#[test]
fn renders_if_either_signal_is_true() {
let (a_sig, set_a) = signal(false);
let (b_sig, set_b) = signal(false);
let mut a = list_with(&[("foo", ClassCondition::when_signal(a_sig))]);
let b = list_with(&[("foo", ClassCondition::when_signal(b_sig))]);
a.merge(b, MergeStrategy::UnionConditions);
assert_that!(a.is_reactive()).is_true();
assert_that!(rendered(&a)).is_equal_to(String::new());
set_a.set(true);
assert_that!(rendered(&a)).is_equal_to("foo".to_string());
set_a.set(false);
set_b.set(true);
assert_that!(rendered(&a)).is_equal_to("foo".to_string());
}
#[test]
fn preserves_entry_order_of_self() {
let mut a = list_with(&[
("first", ClassCondition::always()),
("second", ClassCondition::always()),
("third", ClassCondition::always()),
]);
let b = list_with(&[("second", ClassCondition::always())]);
a.merge(b, MergeStrategy::UnionConditions);
assert_that!(rendered(&a)).is_equal_to("first second third".to_string());
}
#[test]
fn dissolves_self_toggle_when_incoming_single_collides() {
let (s, set_s) = signal(false);
let (o, set_o) = signal(false);
let mut a = ClassList::empty();
a.add_toggle(
ClassCondition::when_signal(s),
ClassName::from("on"),
ClassName::from("off"),
);
let b = list_with(&[("on", ClassCondition::when_signal(o))]);
a.merge(b, MergeStrategy::UnionConditions);
assert_that!(rendered(&a)).is_equal_to("off".to_string());
set_o.set(true);
assert_that!(rendered(&a)).is_equal_to("on off".to_string());
set_s.set(true);
assert_that!(rendered(&a)).is_equal_to("on".to_string());
}
#[test]
fn dissolved_self_toggle_keeps_true_before_false_when_false_branch_collides() {
let (s, set_s) = signal(true);
let (o, set_o) = signal(false);
let mut a = ClassList::empty();
a.add_toggle(
ClassCondition::when_signal(s),
ClassName::from("on"),
ClassName::from("off"),
);
let b = list_with(&[("off", ClassCondition::when_signal(o))]);
a.merge(b, MergeStrategy::UnionConditions);
assert_that!(rendered(&a)).is_equal_to("on".to_string());
set_o.set(true);
assert_that!(rendered(&a)).is_equal_to("on off".to_string());
set_s.set(false);
assert_that!(rendered(&a)).is_equal_to("off".to_string());
}
}
}
}