euv-core 0.3.29

A declarative, cross-platform UI framework for Rust with virtual DOM, reactive signals, and HTML macros for WebAssembly.
Documentation
use crate::*;

/// Creates a reactive style `AttributeValue` that updates when signals change.
///
/// This function replaces the inline `Signal::create(...)` + `subscribe_attr_signal(...)`
/// boilerplate that was previously generated by the `html!` macro for every
/// `style:` attribute containing reactive `if` conditions.
///
/// # Arguments
///
/// - `Fn() -> String + 'static` - A closure that computes the current CSS string.
///   Called on initial render and whenever any signal changes.
///
/// # Returns
///
/// - `AttributeValue` - A `AttributeValue::Signal` backed by a `Signal<String>`
///   that reactively re-evaluates the CSS string on signal updates.
pub fn create_reactive_style_attribute<F>(compute: F) -> AttributeValue
where
    F: Fn() -> String + 'static,
{
    let attr_signal: Signal<String> = Signal::create(compute());
    subscribe_attr_signal(attr_signal, compute);
    AttributeValue::Signal(attr_signal)
}

/// Creates a reactive attribute `AttributeValue` for conditional attribute values.
///
/// This function replaces the inline `Signal::create(...)` + `subscribe_attr_signal(...)`
/// boilerplate that was previously generated by the `html!` macro for every
/// attribute value containing an `if` condition.
///
/// # Arguments
///
/// - `Fn() -> String + 'static` - A closure that computes the current attribute value.
///   Called on initial render and whenever any signal changes.
///
/// # Returns
///
/// - `AttributeValue` - A `AttributeValue::Signal` backed by a `Signal<String>`
///   that reactively re-evaluates the attribute value on signal updates.
pub fn create_reactive_attr_signal<F>(compute: F) -> AttributeValue
where
    F: Fn() -> String + 'static,
{
    let attr_signal: Signal<String> =
        Signal::create(IntoReactiveString::into_reactive_string(compute()));
    subscribe_attr_signal(attr_signal, move || {
        IntoReactiveString::into_reactive_string(compute())
    });
    AttributeValue::Signal(attr_signal)
}

/// Merges multiple class attribute values into a single `AttributeValue`.
///
/// Each input value is adapted into an `AttributeValue` via `IntoReactiveValue`.
/// `CssClass` values are injected into the DOM and their names are collected.
/// All non-empty class names are joined with spaces into a final `Text` attribute.
/// If any value is signal-backed, the result becomes a reactive `Signal` attribute
/// that re-evaluates when any constituent signal changes.
///
/// # Arguments
///
/// - `&[AttributeValue]` - The class attribute values to merge.
///
/// # Returns
///
/// - `AttributeValue` - A merged attribute value containing space-separated class names.
pub fn merge_class_values(values: &[AttributeValue]) -> AttributeValue {
    let has_signal: bool = values
        .iter()
        .any(|value: &AttributeValue| matches!(value, AttributeValue::Signal(_)));
    if has_signal {
        let owned_values: Vec<AttributeValue> = values.to_vec();
        let compute = move || {
            let mut result: String = String::new();
            for value in &owned_values {
                let s: String = match value {
                    AttributeValue::Css(css_class) => {
                        css_class.inject_style();
                        css_class.get_name().to_string()
                    }
                    AttributeValue::Text(s) => s.clone(),
                    AttributeValue::Signal(signal) => signal.get(),
                    _ => String::new(),
                };
                if !s.is_empty() {
                    if !result.is_empty() {
                        result.push(' ');
                    }
                    result.push_str(&s);
                }
            }
            result
        };
        let attr_signal: Signal<String> = Signal::create(compute());
        subscribe_attr_signal(attr_signal, compute);
        AttributeValue::Signal(attr_signal)
    } else {
        let mut result: String = String::new();
        for value in values {
            let s: String = match value {
                AttributeValue::Css(css_class) => {
                    css_class.inject_style();
                    css_class.get_name().to_string()
                }
                AttributeValue::Text(s) => s.clone(),
                _ => String::new(),
            };
            if !s.is_empty() {
                if !result.is_empty() {
                    result.push(' ');
                }
                result.push_str(&s);
            }
        }
        AttributeValue::Text(result)
    }
}

/// Merges multiple style attribute values into a single `AttributeValue`.
///
/// Each input value is expected to be a style string (`Text`) or a reactive
/// `Signal<String>` producing a style string. All non-empty style strings are
/// joined with spaces into a final combined style attribute.
/// If any value is signal-backed, the result becomes a reactive `Signal` attribute.
///
/// # Arguments
///
/// - `&[AttributeValue]` - The style attribute values to merge.
///
/// # Returns
///
/// - `AttributeValue` - A merged attribute value containing the combined CSS style string.
pub fn merge_style_values(values: &[AttributeValue]) -> AttributeValue {
    let has_signal: bool = values
        .iter()
        .any(|value: &AttributeValue| matches!(value, AttributeValue::Signal(_)));
    if has_signal {
        let owned_values: Vec<AttributeValue> = values.to_vec();
        let compute = move || {
            let mut result: String = String::new();
            for value in &owned_values {
                let s: String = match value {
                    AttributeValue::Text(s) => s.clone(),
                    AttributeValue::Signal(signal) => signal.get(),
                    _ => String::new(),
                };
                if !s.is_empty() {
                    if !result.is_empty() {
                        result.push(' ');
                    }
                    result.push_str(&s);
                }
            }
            result
        };
        let attr_signal: Signal<String> = Signal::create(compute());
        subscribe_attr_signal(attr_signal, compute);
        AttributeValue::Signal(attr_signal)
    } else {
        let mut result: String = String::new();
        for value in values {
            let s: String = match value {
                AttributeValue::Text(s) => s.clone(),
                _ => String::new(),
            };
            if !s.is_empty() {
                if !result.is_empty() {
                    result.push(' ');
                }
                result.push_str(&s);
            }
        }
        AttributeValue::Text(result)
    }
}

/// Inserts a class name into the injected classes set.
///
/// # Arguments
///
/// - `String`: The class name to insert.
///
/// # Returns
///
/// - `bool`: `true` if newly inserted, `false` if already present.
#[allow(static_mut_refs)]
pub(crate) fn mark_injected_class(class_name: String) -> bool {
    unsafe {
        if (*INJECTED_CLASSES.get_0().get()).is_none() {
            (*INJECTED_CLASSES.get_0().get()) = Some(HashSet::new());
        }
        (*INJECTED_CLASSES.get_0().get())
            .as_mut()
            .unwrap_unchecked()
            .insert(class_name)
    }
}

/// Clears all injected classes.
#[allow(static_mut_refs)]
pub(crate) fn clear_injected_classes() {
    unsafe {
        if let Some(set) = (*INJECTED_CLASSES.get_0().get()).as_mut() {
            set.clear();
        }
    }
}

/// Resets the injected CSS class tracking set.
///
/// Frees the `INJECTED_CLASSES` `HashSet` and sets the pointer back to null
/// so that CSS classes will be re-injected on the next mount after a browser
/// refresh. Also removes the shared `<style>` element from the DOM.
///
/// # Panics
///
/// Panics if `window()` or `document()` is unavailable on WASM targets.
pub(crate) fn reset_injected_classes() {
    clear_injected_classes();
    let window: Window = window().expect("no global window exists");
    let document: Document = window.document().expect("should have a document");
    if let Some(style_element) = document.get_element_by_id("euv-css-injected") {
        style_element.remove();
    }
}