use std::fmt::Debug;
use std::marker::PhantomData;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use crate::core::{MessageContext, MessageResult, Mut, View, ViewElement, ViewMarker};
use crate::diff::{Diff, diff_iters};
use crate::modifiers::{Modifier, WithModifier};
use crate::vecmap::VecMap;
use crate::{DomView, ViewCtx};
type CowStr = std::borrow::Cow<'static, str>;
#[derive(Debug, PartialEq, Clone)]
pub enum ClassModifier {
Add(CowStr),
Remove(CowStr),
}
impl ClassModifier {
pub fn name(&self) -> &CowStr {
let (Self::Add(name) | Self::Remove(name)) = self;
name
}
}
pub trait ClassIter: PartialEq + Debug + 'static {
fn class_iter(&self) -> impl Iterator<Item = CowStr>;
fn add_class_iter(&self) -> impl Iterator<Item = ClassModifier> {
self.class_iter().map(ClassModifier::Add)
}
fn remove_class_iter(&self) -> impl Iterator<Item = ClassModifier> {
self.class_iter().map(ClassModifier::Remove)
}
}
impl<C: ClassIter> ClassIter for Option<C> {
fn class_iter(&self) -> impl Iterator<Item = CowStr> {
self.iter().flat_map(|c| c.class_iter())
}
}
impl ClassIter for String {
fn class_iter(&self) -> impl Iterator<Item = CowStr> {
std::iter::once(self.clone().into())
}
}
impl ClassIter for &'static str {
fn class_iter(&self) -> impl Iterator<Item = CowStr> {
std::iter::once(CowStr::from(*self))
}
}
impl ClassIter for CowStr {
fn class_iter(&self) -> impl Iterator<Item = Self> {
std::iter::once(self.clone())
}
}
impl<C: ClassIter> ClassIter for Vec<C> {
fn class_iter(&self) -> impl Iterator<Item = CowStr> {
self.iter().flat_map(|c| c.class_iter())
}
}
impl<C: ClassIter, const N: usize> ClassIter for [C; N] {
fn class_iter(&self) -> impl Iterator<Item = CowStr> {
self.iter().flat_map(|c| c.class_iter())
}
}
#[derive(Default)]
pub struct Classes {
class_name: String,
classes: VecMap<CowStr, ()>,
modifiers: Vec<ClassModifier>,
idx: u16,
dirty: bool,
}
impl Classes {
pub(crate) fn new(size_hint: usize) -> Self {
Self {
modifiers: Vec::with_capacity(size_hint),
..Default::default()
}
}
pub fn apply_changes(this: Modifier<'_, Self>, element: &web_sys::Element) {
let Modifier { modifier, flags } = this;
if flags.in_hydration() {
modifier.dirty = false;
} else if modifier.dirty {
modifier.dirty = false;
modifier.classes.clear();
modifier.classes.reserve(modifier.modifiers.len());
for m in &modifier.modifiers {
match m {
ClassModifier::Remove(class_name) => modifier.classes.remove(class_name),
ClassModifier::Add(class_name) => {
modifier.classes.insert(class_name.clone(), ())
}
};
}
modifier.class_name.clear();
modifier
.class_name
.reserve_exact(modifier.classes.keys().map(|k| k.len() + 1).sum());
let last_idx = modifier.classes.len().saturating_sub(1);
for (idx, class) in modifier.classes.keys().enumerate() {
modifier.class_name += class;
if idx != last_idx {
modifier.class_name += " ";
}
}
if element.dyn_ref::<web_sys::SvgElement>().is_some() {
element
.set_attribute(wasm_bindgen::intern("class"), &modifier.class_name)
.unwrap_throw();
} else {
element.set_class_name(&modifier.class_name);
}
}
}
#[inline]
pub fn rebuild<E: WithModifier<Self>>(mut element: E, prev_len: usize, f: impl FnOnce(E)) {
element.modifier().modifier.idx -= prev_len as u16;
f(element);
}
#[inline]
pub fn push(this: &mut Modifier<'_, Self>, modifier: ClassModifier) {
debug_assert!(
this.flags.was_created(),
"This should never be called, when the underlying element wasn't (re)created. Use `Classes::insert` instead."
);
this.modifier.dirty = true;
this.flags.set_needs_update();
this.modifier.modifiers.push(modifier);
this.modifier.idx += 1;
}
#[inline]
pub fn insert(this: &mut Modifier<'_, Self>, modifier: ClassModifier) {
debug_assert!(
!this.flags.was_created(),
"This should never be called, when the underlying element was (re)created, use `Classes::push` instead."
);
this.modifier.dirty = true;
this.flags.set_needs_update();
this.modifier
.modifiers
.insert(this.modifier.idx as usize, modifier);
this.modifier.idx += 1;
}
#[inline]
pub fn mutate<R>(this: &mut Modifier<'_, Self>, f: impl FnOnce(&mut ClassModifier) -> R) -> R {
debug_assert!(
!this.flags.was_created(),
"This should never be called, when the underlying element was (re)created, use `Classes::push` instead."
);
this.modifier.dirty = true;
this.flags.set_needs_update();
let idx = this.modifier.idx;
this.modifier.idx += 1;
f(&mut this.modifier.modifiers[idx as usize])
}
#[inline]
pub fn skip(this: &mut Modifier<'_, Self>, count: usize) {
debug_assert!(
!this.flags.was_created(),
"This should never be called, when the underlying element was (re)created"
);
this.modifier.idx += count as u16;
}
#[inline]
pub fn delete(this: &mut Modifier<'_, Self>, count: usize) {
debug_assert!(
!this.flags.was_created(),
"This should never be called, when the underlying element was (re)created."
);
let start = this.modifier.idx as usize;
this.modifier.dirty = true;
this.flags.set_needs_update();
this.modifier.modifiers.drain(start..(start + count));
}
#[inline]
pub fn extend(
this: &mut Modifier<'_, Self>,
modifiers: impl Iterator<Item = ClassModifier>,
) -> usize {
debug_assert!(
this.flags.was_created(),
"This should never be called, when the underlying element wasn't (re)created, use `Classes::apply_diff` instead."
);
this.modifier.dirty = true;
this.flags.set_needs_update();
let prev_len = this.modifier.modifiers.len();
this.modifier.modifiers.extend(modifiers);
let new_len = this.modifier.modifiers.len() - prev_len;
this.modifier.idx += new_len as u16;
new_len
}
#[inline]
pub fn apply_diff<T: Iterator<Item = ClassModifier>>(
this: &mut Modifier<'_, Self>,
prev: T,
next: T,
) -> usize {
debug_assert!(
!this.flags.was_created(),
"This should never be called, when the underlying element was (re)created, use `Classes::extend` instead."
);
let mut new_len = 0;
for change in diff_iters(prev, next) {
match change {
Diff::Add(modifier) => {
Self::insert(this, modifier);
new_len += 1;
}
Diff::Remove(count) => Self::delete(this, count),
Diff::Change(new_modifier) => {
Self::mutate(this, |modifier| *modifier = new_modifier);
new_len += 1;
}
Diff::Skip(count) => {
Self::skip(this, count);
new_len += count;
}
}
}
new_len
}
pub fn update_as_add_class_iter<T: ClassIter>(
this: &mut Modifier<'_, Self>,
prev_len: usize,
prev: &T,
next: &T,
) -> usize {
if this.flags.was_created() {
Self::extend(this, next.add_class_iter())
} else if next != prev {
Self::apply_diff(this, prev.add_class_iter(), next.add_class_iter())
} else {
Self::skip(this, prev_len);
prev_len
}
}
}
#[derive(Clone, Debug)]
pub struct Class<E, C, T, A> {
el: E,
classes: C,
phantom: PhantomData<fn() -> (T, A)>,
}
impl<E, C, T, A> Class<E, C, T, A> {
pub fn new(el: E, classes: C) -> Self {
Self {
el,
classes,
phantom: PhantomData,
}
}
}
impl<V, C, State, Action> ViewMarker for Class<V, C, State, Action> {}
impl<V, C, State, Action> View<State, Action, ViewCtx> for Class<V, C, State, Action>
where
State: 'static,
Action: 'static,
C: ClassIter,
V: DomView<State, Action, Element: WithModifier<Classes>>,
for<'a> <V::Element as ViewElement>::Mut<'a>: WithModifier<Classes>,
{
type Element = V::Element;
type ViewState = (usize, V::ViewState);
fn build(&self, ctx: &mut ViewCtx, app_state: &mut State) -> (Self::Element, Self::ViewState) {
let add_class_iter = self.classes.add_class_iter();
let (mut e, s) = ctx.with_size_hint::<Classes, _>(add_class_iter.size_hint().0, |ctx| {
self.el.build(ctx, app_state)
});
let len = Classes::extend(&mut e.modifier(), add_class_iter);
(e, (len, s))
}
fn rebuild(
&self,
prev: &Self,
(len, view_state): &mut Self::ViewState,
ctx: &mut ViewCtx,
element: Mut<'_, Self::Element>,
app_state: &mut State,
) {
Classes::rebuild(element, *len, |mut elem| {
self.el
.rebuild(&prev.el, view_state, ctx, elem.reborrow_mut(), app_state);
*len = Classes::update_as_add_class_iter(
&mut elem.modifier(),
*len,
&prev.classes,
&self.classes,
);
});
}
fn teardown(
&self,
(_, view_state): &mut Self::ViewState,
ctx: &mut ViewCtx,
element: Mut<'_, Self::Element>,
) {
self.el.teardown(view_state, ctx, element);
}
fn message(
&self,
(_, view_state): &mut Self::ViewState,
message: &mut MessageContext,
element: Mut<'_, Self::Element>,
app_state: &mut State,
) -> MessageResult<Action> {
self.el.message(view_state, message, element, app_state)
}
}