use dioxus::prelude::*;
pub use dioxus_hoverfx_core::*;
pub const HOVERFX_INIT_HANDLER: &str = "hoverfx.init";
pub const HOVERFX_REFRESH_HANDLER: &str = "hoverfx.refresh";
pub const HOVERFX_RADIUS_HANDLER: &str = "hoverfx.radius";
pub const HOVERFX_SHAPE_HANDLER: &str = "hoverfx.shape";
pub const HOVERFX_FALLOFF_HANDLER: &str = "hoverfx.falloff";
pub const HOVERFX_STRENGTH_HANDLER: &str = "hoverfx.strength";
pub const HOVERFX_THEME_CHANGE_EVENT: &str = dioxus_theme_core::THEME_CHANGE_EVENT;
const DEFAULT_EFFECT: &str = "spotlight";
const DEFAULT_RADIUS_MIN: u16 = 80;
const DEFAULT_RADIUS_MAX: u16 = 640;
const DEFAULT_RADIUS_STEP: u16 = 10;
const DEFAULT_STRENGTH_PERCENT: u16 = 100;
const DEFAULT_STRENGTH_MIN: u16 = 0;
const DEFAULT_STRENGTH_MAX: u16 = 200;
const DEFAULT_STRENGTH_STEP: u16 = 5;
const HOVERFX_WALLPAPER_LAYER_STYLE: &str = concat!(
"position:absolute!important;",
"inset:0!important;",
"z-index:0!important;",
"display:block!important;",
"inline-size:100%!important;",
"block-size:100%!important;",
"width:100%!important;",
"height:100%!important;",
"min-inline-size:0!important;",
"min-block-size:0!important;",
"min-width:0!important;",
"min-height:0!important;",
"max-inline-size:none!important;",
"max-block-size:none!important;",
"max-width:none!important;",
"max-height:none!important;",
"margin:0!important;",
"padding:0!important;",
"border:0!important;",
"box-sizing:border-box!important;",
"border-radius:inherit!important;",
"overflow:hidden!important;",
"pointer-events:none!important;",
"contain:layout paint style!important;",
"flex:none!important;",
"align-self:stretch!important;",
"grid-area:1 / 1!important;",
"mix-blend-mode:var(--dxh-text-layer-blend-mode,var(--dxh-layer-blend-mode,var(--dxh-blend-mode,normal)))!important;"
);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HoverFxThemeTokenInterop {
pub change_event: &'static str,
pub accent_token: &'static str,
pub muted_token: &'static str,
pub surface_token: &'static str,
pub text_token: &'static str,
pub sand_color_token: &'static str,
pub sand_highlight_token: &'static str,
}
pub const fn hoverfx_theme_token_interop() -> HoverFxThemeTokenInterop {
HoverFxThemeTokenInterop {
change_event: dioxus_theme_core::THEME_CHANGE_EVENT,
accent_token: dioxus_theme_core::THEME_TOKEN_ACCENT,
muted_token: dioxus_theme_core::THEME_TOKEN_MUTED,
surface_token: dioxus_theme_core::THEME_TOKEN_SURFACE,
text_token: dioxus_theme_core::THEME_TOKEN_TEXT,
sand_color_token: dioxus_theme_core::THEME_TOKEN_SURFACE,
sand_highlight_token: dioxus_theme_core::THEME_TOKEN_ACCENT,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HoverFxRuntimeMode {
BrowserRuntime,
StaticFallback,
}
pub fn hoverfx_runtime_mode(_config: &HoverFxConfig) -> HoverFxRuntimeMode {
if cfg!(all(feature = "web", target_arch = "wasm32")) {
HoverFxRuntimeMode::BrowserRuntime
} else {
HoverFxRuntimeMode::StaticFallback
}
}
pub fn hoverfx_native_fallback_config() -> HoverFxConfig {
HoverFxConfig::default()
}
pub fn hoverfx_native_compatibility_manifest() -> dioxus_native_port::VisualCompatibilityManifest {
dioxus_native_port::native_port_visual_compatibility_manifest("dioxus-hoverfx")
.expect("dioxus-hoverfx visual compatibility manifest is registered")
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HoverFxNativeAction {
Init,
Refresh,
SetRadius,
SetShape,
SetFalloff,
SetStrength,
}
impl HoverFxNativeAction {
pub const fn as_str(self) -> &'static str {
match self {
Self::Init => "init",
Self::Refresh => "refresh",
Self::SetRadius => "set-radius",
Self::SetShape => "set-shape",
Self::SetFalloff => "set-falloff",
Self::SetStrength => "set-strength",
}
}
pub const fn handler(self) -> &'static str {
match self {
Self::Init => HOVERFX_INIT_HANDLER,
Self::Refresh => HOVERFX_REFRESH_HANDLER,
Self::SetRadius => HOVERFX_RADIUS_HANDLER,
Self::SetShape => HOVERFX_SHAPE_HANDLER,
Self::SetFalloff => HOVERFX_FALLOFF_HANDLER,
Self::SetStrength => HOVERFX_STRENGTH_HANDLER,
}
}
pub const fn label(self) -> &'static str {
match self {
Self::Init => "Initialize HoverFX",
Self::Refresh => "Refresh HoverFX targets",
Self::SetRadius => "Set hover radius",
Self::SetShape => "Set hover shape",
Self::SetFalloff => "Set hover falloff",
Self::SetStrength => "Set hover strength",
}
}
}
pub fn hoverfx_native_package_actions(
route: Option<&str>,
) -> Vec<dioxus_native_port::NativePackageAction> {
let route = route.map(str::to_string);
[
HoverFxNativeAction::Init,
HoverFxNativeAction::Refresh,
HoverFxNativeAction::SetRadius,
HoverFxNativeAction::SetShape,
HoverFxNativeAction::SetFalloff,
HoverFxNativeAction::SetStrength,
]
.into_iter()
.map(|action| {
let mut package_action = dioxus_native_port::NativePackageAction::new(
"dioxus-hoverfx",
action.as_str(),
action.label(),
dioxus_native_port::NativeActionKind::NativeAction,
)
.description(format!(
"Controls worker-first cursor hover effects without page hydration. Handler: {}.",
action.handler()
));
if let Some(route) = route.clone() {
package_action = package_action.route(route);
}
package_action
})
.collect()
}
pub fn hoverfx_native_action(
config: &HoverFxConfig,
action: HoverFxNativeAction,
) -> dioxus_native_port::NativeActionResult {
let mode = hoverfx_runtime_mode(config);
let backend = match mode {
HoverFxRuntimeMode::BrowserRuntime => "browser-runtime",
HoverFxRuntimeMode::StaticFallback => "static-fallback",
};
dioxus_native_port::NativeActionResult::succeeded(
"dioxus-hoverfx",
action.as_str(),
dioxus_native_port::NativeActionKind::NativeAction,
format!("{} prepared", action.label()),
)
.with_backend(backend)
.with_output("handler", action.handler())
}
pub fn hoverfx_preset_attr(preset: HoverFxPreset) -> &'static str {
preset.as_attr()
}
pub fn hoverfx_preset_label(preset: HoverFxPreset) -> &'static str {
preset.label()
}
pub fn hoverfx_shape_attr(shape: HoverFxShape) -> &'static str {
shape.as_attr()
}
pub fn hoverfx_shape_label(shape: HoverFxShape) -> &'static str {
shape.label()
}
pub fn hoverfx_falloff_attr(falloff: HoverFxFalloff) -> &'static str {
falloff.as_attr()
}
pub fn hoverfx_falloff_label(falloff: HoverFxFalloff) -> &'static str {
falloff.label()
}
pub fn hoverfx_text_contrast_attr(text_contrast: HoverFxTextContrastMode) -> &'static str {
text_contrast.as_attr()
}
fn hoverfx_control_id(prefix: &str, handler: &str, label: &str) -> String {
format!("{prefix}-{}", hoverfx_id(format!("{handler}-{label}")))
}
fn sanitized_effect(effect: &str, preset: Option<HoverFxPreset>) -> String {
preset
.map(|preset| hoverfx_preset_attr(preset).to_string())
.unwrap_or_else(|| hoverfx_id(effect.to_string()))
}
fn optional_u16_attr(value: Option<u16>) -> String {
value.map(|value| value.to_string()).unwrap_or_default()
}
fn optional_f32_attr(value: Option<f32>) -> String {
value.map(format_float).unwrap_or_default()
}
fn bool_attr(value: bool) -> &'static str {
if value { "true" } else { "false" }
}
fn format_float(value: f32) -> String {
let mut formatted = format!("{value:.3}");
while formatted.contains('.') && formatted.ends_with('0') {
formatted.pop();
}
if formatted.ends_with('.') {
formatted.pop();
}
formatted
}
fn optional_shape_attr(value: Option<HoverFxShape>) -> String {
value
.map(hoverfx_shape_attr)
.unwrap_or_default()
.to_string()
}
fn optional_falloff_attr(value: Option<HoverFxFalloff>) -> String {
value
.map(hoverfx_falloff_attr)
.unwrap_or_default()
.to_string()
}
fn optional_text_reveal_attr(value: Option<&HoverFxTextRevealConfig>) -> String {
value
.and_then(|value| value.to_json().ok())
.unwrap_or_default()
}
fn optional_texture_reveal_attr(value: Option<&HoverFxTextureRevealConfig>) -> String {
value
.and_then(|value| value.to_json().ok())
.unwrap_or_default()
}
fn optional_sand_attr(value: Option<&HoverFxSandConfig>) -> String {
value
.and_then(|value| value.to_json().ok())
.unwrap_or_default()
}
fn optional_text_contrast_attr(value: Option<HoverFxTextContrastMode>) -> String {
value
.map(hoverfx_text_contrast_attr)
.unwrap_or_default()
.to_string()
}
fn optional_textfx_config_attr(
value: Option<&HoverFxTextRevealConfig>,
interop: bool,
id: &str,
) -> String {
if !interop {
return String::new();
}
let Some(value) = value else {
return String::new();
};
if value.animation_source == HoverFxTextAnimationSource::HoverFx {
return String::new();
}
value
.to_textfx_config_json(
if id.trim().is_empty() {
"hoverfx-binary-reveal"
} else {
id
},
"010101001101",
)
.unwrap_or_default()
}
#[derive(Props, Clone, PartialEq)]
pub struct HoverFxProviderProps {
#[props(default)]
pub config: HoverFxConfig,
#[props(default)]
pub class: String,
#[props(default = "document".to_string())]
pub scope: String,
#[props(default = true)]
pub auto_init: bool,
pub children: Element,
}
#[component]
pub fn HoverFxProvider(props: HoverFxProviderProps) -> Element {
let mode = hoverfx_runtime_mode(&props.config);
let runtime = match mode {
HoverFxRuntimeMode::BrowserRuntime => "browser-runtime",
HoverFxRuntimeMode::StaticFallback => "static-fallback",
};
let auto_init = if props.auto_init { "true" } else { "false" };
let default_effect = hoverfx_id(&props.config.default_effect);
let radius = props.config.radius_px.to_string();
let shape = props.config.shape.as_attr();
let falloff = props.config.falloff.as_attr();
let strength = format_float(props.config.strength);
let smoothing = format_float(props.config.smoothing);
let max_active = props.config.max_active_elements.to_string();
let renderer = props.config.renderer.as_attr();
let runtime_path = props.config.runtime_path.clone();
let worker_path = props.config.worker_path.clone();
let theme_tokens = hoverfx_theme_token_interop();
rsx! {
div {
class: "{props.class}",
"data-dxh-provider": "true",
"data-dxh-scope": "{props.scope}",
"data-dxh-theme-event": "{theme_tokens.change_event}",
"data-dxh-theme-accent-token": "{theme_tokens.accent_token}",
"data-dxh-theme-muted-token": "{theme_tokens.muted_token}",
"data-dxh-theme-surface-token": "{theme_tokens.surface_token}",
"data-dxh-theme-text-token": "{theme_tokens.text_token}",
"data-dxh-theme-sand-color-token": "{theme_tokens.sand_color_token}",
"data-dxh-theme-sand-highlight-token": "{theme_tokens.sand_highlight_token}",
"data-dxh-runtime": "{runtime}",
"data-dxh-renderer": "{renderer}",
"data-dxh-auto-init": "{auto_init}",
"data-dxh-default-effect": "{default_effect}",
"data-dxh-radius": "{radius}",
"data-dxh-shape": "{shape}",
"data-dxh-falloff": "{falloff}",
"data-dxh-strength": "{strength}",
"data-dxh-smoothing": "{smoothing}",
"data-dxh-max-active-elements": "{max_active}",
"data-dxh-runtime-path": "{runtime_path}",
"data-dxh-worker-path": "{worker_path}",
{props.children}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct HoverFxTargetProps {
#[props(default = DEFAULT_EFFECT.to_string())]
pub effect: String,
#[props(default)]
pub preset: Option<HoverFxPreset>,
#[props(default)]
pub radius: Option<u16>,
#[props(default)]
pub shape: Option<HoverFxShape>,
#[props(default)]
pub falloff: Option<HoverFxFalloff>,
#[props(default)]
pub strength: Option<f32>,
#[props(default)]
pub contained: bool,
#[props(default)]
pub controlled: bool,
#[props(default)]
pub text_reveal: Option<HoverFxTextRevealConfig>,
#[props(default)]
pub texture_reveal: Option<HoverFxTextureRevealConfig>,
#[props(default)]
pub sand: Option<HoverFxSandConfig>,
#[props(default)]
pub text_contrast: Option<HoverFxTextContrastMode>,
#[props(default)]
pub textfx_interop: bool,
#[props(default)]
pub class: String,
#[props(default)]
pub id: String,
pub children: Element,
}
#[component]
pub fn HoverFxTarget(props: HoverFxTargetProps) -> Element {
let effect = sanitized_effect(&props.effect, props.preset);
let radius = optional_u16_attr(props.radius);
let shape = optional_shape_attr(props.shape);
let falloff = optional_falloff_attr(props.falloff);
let strength = optional_f32_attr(props.strength);
let contained = bool_attr(props.contained);
let controlled = bool_attr(props.controlled);
let text_reveal = optional_text_reveal_attr(props.text_reveal.as_ref());
let texture_reveal = optional_texture_reveal_attr(props.texture_reveal.as_ref());
let sand = optional_sand_attr(props.sand.as_ref());
let text_contrast = optional_text_contrast_attr(props.text_contrast);
let textfx_config =
optional_textfx_config_attr(props.text_reveal.as_ref(), props.textfx_interop, &props.id);
let wallpaper_layer_style = HOVERFX_WALLPAPER_LAYER_STYLE;
if textfx_config.is_empty() {
rsx! {
div {
id: "{props.id}",
class: "{props.class}",
"data-dxh-target": "true",
"data-dxh-effect": "{effect}",
"data-dxh-radius": "{radius}",
"data-dxh-shape": "{shape}",
"data-dxh-falloff": "{falloff}",
"data-dxh-strength": "{strength}",
"data-dxh-contain": "{contained}",
"data-dxh-controlled": "{controlled}",
"data-dxh-text-reveal": "{text_reveal}",
"data-dxh-texture-reveal": "{texture_reveal}",
"data-dxh-sand": "{sand}",
"data-dxh-text-contrast": "{text_contrast}",
{props.children}
}
}
} else {
rsx! {
div {
id: "{props.id}",
class: "{props.class}",
"data-dxh-target": "true",
"data-dxh-effect": "{effect}",
"data-dxh-radius": "{radius}",
"data-dxh-shape": "{shape}",
"data-dxh-falloff": "{falloff}",
"data-dxh-strength": "{strength}",
"data-dxh-contain": "{contained}",
"data-dxh-controlled": "{controlled}",
"data-dxh-text-reveal": "{text_reveal}",
"data-dxh-texture-reveal": "{texture_reveal}",
"data-dxh-sand": "{sand}",
"data-dxh-text-contrast": "{text_contrast}",
"data-dxr-on-pointerover": "textfx.run",
span {
class: "dxh-text-reveal-layer",
style: "{wallpaper_layer_style}",
"aria-hidden": "true",
"data-dxh-text-layer": "true",
"data-dxt-textfx": "{textfx_config}"
}
{props.children}
}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct HoverFxCardProps {
#[props(default = DEFAULT_EFFECT.to_string())]
pub effect: String,
#[props(default)]
pub preset: Option<HoverFxPreset>,
#[props(default)]
pub radius: Option<u16>,
#[props(default)]
pub shape: Option<HoverFxShape>,
#[props(default)]
pub falloff: Option<HoverFxFalloff>,
#[props(default)]
pub strength: Option<f32>,
#[props(default)]
pub contained: bool,
#[props(default)]
pub controlled: bool,
#[props(default)]
pub text_reveal: Option<HoverFxTextRevealConfig>,
#[props(default)]
pub texture_reveal: Option<HoverFxTextureRevealConfig>,
#[props(default)]
pub sand: Option<HoverFxSandConfig>,
#[props(default)]
pub text_contrast: Option<HoverFxTextContrastMode>,
#[props(default)]
pub textfx_interop: bool,
#[props(default)]
pub class: String,
#[props(default)]
pub id: String,
pub children: Element,
}
#[component]
pub fn HoverFxCard(props: HoverFxCardProps) -> Element {
rsx! {
HoverFxTarget {
id: props.id,
class: props.class,
effect: props.effect,
preset: props.preset,
radius: props.radius,
shape: props.shape,
falloff: props.falloff,
strength: props.strength,
contained: props.contained,
controlled: props.controlled,
text_reveal: props.text_reveal,
texture_reveal: props.texture_reveal,
sand: props.sand,
text_contrast: props.text_contrast,
textfx_interop: props.textfx_interop,
div {
"data-dxh-card": "true",
{props.children}
}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct HoverFxRadiusSliderProps {
#[props(default = HOVERFX_RADIUS_HANDLER.to_string())]
pub handler: String,
#[props(default)]
pub class: String,
#[props(default = "Hover radius".to_string())]
pub label: String,
#[props(default = DEFAULT_HOVERFX_RADIUS_PX)]
pub value: u16,
#[props(default = DEFAULT_RADIUS_MIN)]
pub min: u16,
#[props(default = DEFAULT_RADIUS_MAX)]
pub max: u16,
#[props(default = DEFAULT_RADIUS_STEP)]
pub step: u16,
#[props(default)]
pub apply_to_all: bool,
}
#[component]
pub fn HoverFxRadiusSlider(props: HoverFxRadiusSliderProps) -> Element {
let input_id = hoverfx_control_id("dxh-radius", &props.handler, &props.label);
let output_id = format!("{input_id}-output");
let min = props.min.min(props.max);
let max = props.max.max(props.min);
let value = props.value.clamp(min, max);
let step = props.step.max(1);
let apply_to = if props.apply_to_all { "all" } else { "target" };
rsx! {
label {
class: "{props.class}",
"data-dxh-control": "radius",
"data-dxh-apply-to": "{apply_to}",
"for": "{input_id}",
span {
"{props.label}: "
output {
id: "{output_id}",
"for": "{input_id}",
"aria-live": "polite",
"data-dxh-radius-current": "true",
"{value}px"
}
}
input {
id: "{input_id}",
r#type: "range",
min: "{min}",
max: "{max}",
step: "{step}",
value: "{value}",
"aria-describedby": "{output_id}",
"data-dxr-on-input": "{props.handler}",
"data-dxh-radius-control": "true"
}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct HoverFxShapeSelectProps {
#[props(default = HOVERFX_SHAPE_HANDLER.to_string())]
pub handler: String,
#[props(default)]
pub class: String,
#[props(default = "Hover shape".to_string())]
pub label: String,
#[props(default = HoverFxShape::Circle)]
pub value: HoverFxShape,
#[props(default)]
pub apply_to_all: bool,
}
#[component]
pub fn HoverFxShapeSelect(props: HoverFxShapeSelectProps) -> Element {
let select_id = hoverfx_control_id("dxh-shape", &props.handler, &props.label);
let label_id = format!("{select_id}-label");
let current_id = format!("{select_id}-current");
let current = hoverfx_shape_attr(props.value);
let current_label = hoverfx_shape_label(props.value);
let apply_to = if props.apply_to_all { "all" } else { "target" };
rsx! {
label {
class: "{props.class}",
"data-dxh-control": "shape",
"data-dxh-apply-to": "{apply_to}",
"for": "{select_id}",
span {
id: "{label_id}",
"{props.label}"
}
select {
id: "{select_id}",
value: "{current}",
"aria-labelledby": "{label_id} {current_id}",
"data-dxr-on-change": "{props.handler}",
"data-dxh-shape-control": "true",
for shape in [
HoverFxShape::Circle,
HoverFxShape::Square,
HoverFxShape::RoundedRect,
HoverFxShape::Polygon,
] {
option {
value: "{hoverfx_shape_attr(shape)}",
selected: shape == props.value,
"{hoverfx_shape_label(shape)}"
}
}
}
span {
id: "{current_id}",
"aria-live": "polite",
"data-dxh-shape-current": "true",
"{current_label}"
}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct HoverFxFalloffSelectProps {
#[props(default = HOVERFX_FALLOFF_HANDLER.to_string())]
pub handler: String,
#[props(default)]
pub class: String,
#[props(default = "Hover falloff".to_string())]
pub label: String,
#[props(default = HoverFxFalloff::Smooth)]
pub value: HoverFxFalloff,
#[props(default)]
pub apply_to_all: bool,
}
#[component]
pub fn HoverFxFalloffSelect(props: HoverFxFalloffSelectProps) -> Element {
let select_id = hoverfx_control_id("dxh-falloff", &props.handler, &props.label);
let label_id = format!("{select_id}-label");
let current_id = format!("{select_id}-current");
let current = hoverfx_falloff_attr(props.value);
let current_label = hoverfx_falloff_label(props.value);
let apply_to = if props.apply_to_all { "all" } else { "target" };
rsx! {
label {
class: "{props.class}",
"data-dxh-control": "falloff",
"data-dxh-apply-to": "{apply_to}",
"for": "{select_id}",
span {
id: "{label_id}",
"{props.label}"
}
select {
id: "{select_id}",
value: "{current}",
"aria-labelledby": "{label_id} {current_id}",
"data-dxr-on-change": "{props.handler}",
"data-dxh-falloff-control": "true",
for falloff in [
HoverFxFalloff::Hard,
HoverFxFalloff::Linear,
HoverFxFalloff::Smooth,
HoverFxFalloff::Exponential,
] {
option {
value: "{hoverfx_falloff_attr(falloff)}",
selected: falloff == props.value,
"{hoverfx_falloff_label(falloff)}"
}
}
}
span {
id: "{current_id}",
"aria-live": "polite",
"data-dxh-falloff-current": "true",
"{current_label}"
}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct HoverFxStrengthSliderProps {
#[props(default = HOVERFX_STRENGTH_HANDLER.to_string())]
pub handler: String,
#[props(default)]
pub class: String,
#[props(default = "Hover strength".to_string())]
pub label: String,
#[props(default = DEFAULT_STRENGTH_PERCENT)]
pub value: u16,
#[props(default = DEFAULT_STRENGTH_MIN)]
pub min: u16,
#[props(default = DEFAULT_STRENGTH_MAX)]
pub max: u16,
#[props(default = DEFAULT_STRENGTH_STEP)]
pub step: u16,
#[props(default)]
pub apply_to_all: bool,
}
#[component]
pub fn HoverFxStrengthSlider(props: HoverFxStrengthSliderProps) -> Element {
let input_id = hoverfx_control_id("dxh-strength", &props.handler, &props.label);
let output_id = format!("{input_id}-output");
let min = props.min.min(props.max);
let max = props.max.max(props.min);
let value = props.value.clamp(min, max);
let step = props.step.max(1);
let apply_to = if props.apply_to_all { "all" } else { "target" };
rsx! {
label {
class: "{props.class}",
"data-dxh-control": "strength",
"data-dxh-apply-to": "{apply_to}",
"for": "{input_id}",
span {
"{props.label}: "
output {
id: "{output_id}",
"for": "{input_id}",
"aria-live": "polite",
"data-dxh-strength-current": "true",
"{value}%"
}
}
input {
id: "{input_id}",
r#type: "range",
min: "{min}",
max: "{max}",
step: "{step}",
value: "{value}",
"aria-describedby": "{output_id}",
"data-dxr-on-input": "{props.handler}",
"data-dxh-strength-control": "true"
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn native_actions_include_expected_handlers() {
let actions = hoverfx_native_package_actions(Some("/hoverfx"));
let manifest = hoverfx_native_compatibility_manifest();
assert_eq!(manifest.package, "dioxus-hoverfx");
assert!(actions.iter().any(|action| action.action == "init"));
assert!(actions.iter().any(|action| action.action == "refresh"));
assert!(actions.iter().any(|action| action.action == "set-radius"));
assert!(actions.iter().any(|action| action.action == "set-shape"));
assert!(actions.iter().any(|action| action.action == "set-falloff"));
assert!(actions.iter().any(|action| action.action == "set-strength"));
assert!(
actions
.iter()
.any(|action| action.description.contains(HOVERFX_RADIUS_HANDLER))
);
}
#[test]
fn helpers_emit_runtime_attribute_values() {
assert_eq!(hoverfx_preset_attr(HoverFxPreset::SoftGlow), "soft-glow");
assert_eq!(
hoverfx_preset_attr(HoverFxPreset::BinaryReveal),
"binary-reveal"
);
assert_eq!(
hoverfx_preset_attr(HoverFxPreset::TextureReveal),
"texture-reveal"
);
assert_eq!(hoverfx_preset_attr(HoverFxPreset::Sand), "sand");
assert_eq!(
hoverfx_shape_attr(HoverFxShape::RoundedRect),
"rounded-rect"
);
assert_eq!(
hoverfx_falloff_attr(HoverFxFalloff::Exponential),
"exponential"
);
assert_eq!(
hoverfx_text_contrast_attr(HoverFxTextContrastMode::Invert),
"invert"
);
}
#[test]
fn text_reveal_attrs_emit_hoverfx_and_optional_textfx_config() {
let config = HoverFxTextRevealConfig::default();
let hoverfx = optional_text_reveal_attr(Some(&config));
assert!(hoverfx.contains(r#""charset":"01""#));
assert!(hoverfx.contains(r#""animationSource":"auto""#));
assert!(hoverfx.contains(r#""renderer":"glyph-atlas""#));
let textfx = optional_textfx_config_attr(Some(&config), true, "binary-card");
assert!(textfx.contains(r#""effect":"scramble""#));
assert!(textfx.contains(r#""charset":"01""#));
assert!(textfx.contains(r#""speedMs":220"#));
assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("position:absolute!important"));
assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("inset:0!important"));
assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("flex:none!important"));
assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("grid-area:1 / 1!important"));
assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("max-width:none!important"));
assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("max-height:none!important"));
assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("pointer-events:none!important"));
assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("contain:layout paint style!important"));
assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("mix-blend-mode:var("));
assert!(optional_textfx_config_attr(Some(&config), false, "binary-card").is_empty());
assert!(
optional_textfx_config_attr(
Some(
&config
.clone()
.with_animation_source(HoverFxTextAnimationSource::HoverFx)
),
true,
"binary-card"
)
.is_empty()
);
}
#[test]
fn texture_reveal_attr_emits_hoverfx_config() {
let config =
HoverFxTextureRevealConfig::default().with_mode(HoverFxTextureRevealMode::Halftone);
let hoverfx = optional_texture_reveal_attr(Some(&config));
assert!(hoverfx.contains(r#""mode":"halftone""#));
assert!(optional_texture_reveal_attr(None).is_empty());
}
#[test]
fn sand_attr_emits_hoverfx_config() {
let config = HoverFxSandConfig::default()
.with_grain_size_px(1.4)
.with_shimmer_strength(0.9)
.with_shimmer_radius_px(280.0)
.with_specular_strength(1.2)
.with_color_source(HoverFxSandColorSource::Element)
.with_animation_speed_ms(720);
let hoverfx = optional_sand_attr(Some(&config));
assert!(hoverfx.contains(r#""grainSizePx":"#));
assert!(hoverfx.contains(r#""shimmerStrength":"#));
assert!(hoverfx.contains(r#""shimmerRadiusPx":"#));
assert!(hoverfx.contains(r#""specularStrength":"#));
assert!(hoverfx.contains(r#""colorSource":"element""#));
assert!(hoverfx.contains(r#""animationSpeedMs":720"#));
assert!(optional_sand_attr(None).is_empty());
}
#[test]
fn text_contrast_attr_emits_mode() {
assert_eq!(
optional_text_contrast_attr(Some(HoverFxTextContrastMode::Auto)),
"auto"
);
assert_eq!(
optional_text_contrast_attr(Some(HoverFxTextContrastMode::Darken)),
"darken"
);
assert_eq!(
optional_text_contrast_attr(Some(HoverFxTextContrastMode::Invert)),
"invert"
);
assert!(optional_text_contrast_attr(None).is_empty());
}
#[test]
fn theme_token_interop_metadata_uses_shared_contract() {
let interop = hoverfx_theme_token_interop();
assert_eq!(interop.change_event, "dioxus-theme:change");
assert_eq!(interop.accent_token, dioxus_theme_core::THEME_TOKEN_ACCENT);
assert_eq!(
interop.surface_token,
dioxus_theme_core::THEME_TOKEN_SURFACE
);
assert_eq!(
interop.sand_color_token,
dioxus_theme_core::THEME_TOKEN_SURFACE
);
assert_eq!(
interop.sand_highlight_token,
dioxus_theme_core::THEME_TOKEN_ACCENT
);
}
#[test]
fn native_action_reports_handler() {
let result =
hoverfx_native_action(&HoverFxConfig::default(), HoverFxNativeAction::SetRadius);
assert_eq!(
result.outputs.get("handler"),
Some(&HOVERFX_RADIUS_HANDLER.to_string())
);
}
}