xilem/xilem_web/src/modifiers/class.rs

394 lines
14 KiB
Rust

// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Debug;
use std::marker::PhantomData;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use crate::core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker};
use crate::diff::{Diff, diff_iters};
use crate::modifiers::{Modifier, WithModifier};
use crate::vecmap::VecMap;
use crate::{DomView, DynMessage, ViewCtx};
type CowStr = std::borrow::Cow<'static, str>;
#[derive(Debug, PartialEq, Clone)]
/// An modifier element to either add or remove a class of an element.
///
/// It's used in [`Classes`].
pub enum ClassModifier {
Add(CowStr),
Remove(CowStr),
}
impl ClassModifier {
/// Returns the class name of this modifier.
pub fn name(&self) -> &CowStr {
let (Self::Add(name) | Self::Remove(name)) = self;
name
}
}
/// Types implementing this trait can be used in the [`Class`] view, see also [`Element::class`](`crate::interfaces::Element::class`).
pub trait ClassIter: PartialEq + Debug + 'static {
/// Returns an iterator of class compliant strings (e.g. the strings aren't allowed to contain spaces).
fn class_iter(&self) -> impl Iterator<Item = CowStr>;
/// Returns an iterator of additive classes, i.e. all classes of this iterator are added to the current element.
fn add_class_iter(&self) -> impl Iterator<Item = ClassModifier> {
self.class_iter().map(ClassModifier::Add)
}
/// Returns an iterator of to remove classes, i.e. all classes of this iterator are removed from the current element.
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)]
/// An Element modifier that manages all classes of an Element.
pub struct Classes {
class_name: String,
// It would be nice to avoid this, as this results in extra allocations, when `Strings` are used as classes.
classes: VecMap<CowStr, ()>,
modifiers: Vec<ClassModifier>,
idx: u16,
dirty: bool,
}
impl Classes {
/// Creates a new `Classes` modifier.
///
/// `size_hint` is used to avoid unnecessary allocations while traversing up the view-tree when adding modifiers in [`View::build`].
pub(crate) fn new(size_hint: usize) -> Self {
Self {
modifiers: Vec::with_capacity(size_hint),
..Default::default()
}
}
/// Applies potential changes of the classes of an element to the underlying DOM node.
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 += " ";
}
}
// Svg elements do have issues with className, see https://developer.mozilla.org/en-US/docs/Web/API/Element/className
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]
/// Rebuilds the current element, while ensuring that the order of the modifiers stays correct.
/// Any children should be rebuilt in inside `f`, *before* modifying any other properties of [`Classes`].
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]
/// Pushes `modifier` at the end of the current modifiers.
///
/// Must only be used when `this.flags.was_created() == true`
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]
/// Inserts `modifier` at the current index.
///
/// Must only be used when `this.flags.was_created() == false`
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();
// TODO this could potentially be expensive, maybe think about `VecSplice` again.
// Although in the average case, this is likely not relevant, as usually very few attributes are used, thus shifting is probably good enough
// I.e. a `VecSplice` is probably less optimal (either more complicated code, and/or more memory usage)
this.modifier
.modifiers
.insert(this.modifier.idx as usize, modifier);
this.modifier.idx += 1;
}
#[inline]
/// Mutates the next modifier.
///
/// Must only be used when `this.flags.was_created() == false`
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]
/// Skips the next `count` modifiers.
///
/// Must only be used when `this.flags.was_created() == false`
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]
/// Deletes the next `count` modifiers.
///
/// Must only be used when `this.flags.was_created() == false`
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]
/// Extends the current modifiers with an iterator of modifiers. Returns the count of `modifiers`.
///
/// Must only be used when `this.flags.was_created() == true`
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]
/// Diffs between two iterators, and updates the underlying modifiers if they have changed, returns the `next` iterator count.
///
/// Must only be used when `this.flags.was_created() == false`
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
}
/// Updates based on the diff between two class iterators (`prev`, `next`) interpreted as add modifiers.
///
/// Updates the underlying modifiers if they have changed, returns the next iterator count.
/// Skips or adds modifiers, when nothing has changed, or the element was recreated.
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
}
}
}
/// A view to add classes to `Element` derived elements.
///
/// See [`Element::class`](`crate::interfaces::Element::class`) for more usage information.
#[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> {
/// Create a `Class` view. `classes` is a [`ClassIter`].
///
/// Usually [`Element::class`](`crate::interfaces::Element::class`) should be used instead of this function.
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, DynMessage> 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) -> (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));
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>,
) {
Classes::rebuild(element, *len, |mut elem| {
self.el
.rebuild(&prev.el, view_state, ctx, elem.reborrow_mut());
*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,
id_path: &[ViewId],
message: DynMessage,
app_state: &mut State,
) -> MessageResult<Action, DynMessage> {
self.el.message(view_state, id_path, message, app_state)
}
}