1use dioxus::prelude::*;
2pub use dioxus_hoverfx_core::*;
3
4pub use HoverFxCard as Card;
5pub use HoverFxFalloffSelect as FalloffSelect;
6pub use HoverFxProvider as HoverProvider;
7pub use HoverFxRadiusSlider as RadiusSlider;
8pub use HoverFxRangeSlider as RangeSlider;
9pub use HoverFxShapeSelect as ShapeSelect;
10pub use HoverFxStrengthSlider as StrengthSlider;
11pub use HoverFxTarget as Target;
12
13pub mod prelude {
14 pub use dioxus_hoverfx_core::prelude::*;
15
16 pub use crate::{
17 Card, FalloffSelect, HoverCfg, HoverDef, HoverFxCard, HoverFxConfig, HoverFxFalloff,
18 HoverFxPreset, HoverFxProvider, HoverFxRangeSlider, HoverFxRenderer, HoverFxShape,
19 HoverFxTarget, HoverFxTooltipConfig, HoverFxTooltipPlacement, HoverProvider, RadiusSlider,
20 RangeSlider, ShapeSelect, StrengthSlider, Target, hover_def, hover_fx, hoverfx,
21 hoverfx_component_explain, hoverfx_component_manifest, hoverfx_native_integration_hints,
22 };
23}
24
25pub mod dx {
26 pub use crate::prelude::*;
27
28 pub fn hoverfx() -> dioxus_hoverfx_core::HoverFxConfig {
29 dioxus_hoverfx_core::HoverFxConfig::new()
30 }
31
32 pub fn tooltip(text: impl Into<String>) -> dioxus_hoverfx_core::HoverFxTooltipConfig {
33 dioxus_hoverfx_core::HoverFxTooltipConfig::new(text)
34 }
35
36 pub fn tooltip_key(
37 key: impl AsRef<str>,
38 fallback: impl Into<String>,
39 ) -> dioxus_hoverfx_core::HoverFxTooltipConfig {
40 dioxus_hoverfx_core::HoverFxTooltipConfig::new(fallback).key(key)
41 }
42
43 pub fn localized_tooltip(
44 key: impl AsRef<str>,
45 fallback: impl Into<String>,
46 ) -> dioxus_hoverfx_core::HoverFxTooltipConfig {
47 tooltip_key(key, fallback)
48 }
49
50 pub fn effect(
51 id: impl AsRef<str>,
52 label: impl Into<String>,
53 ) -> dioxus_hoverfx_core::HoverFxDefinition {
54 dioxus_hoverfx_core::HoverFxDefinition::new(id, label)
55 }
56
57 pub trait HoverFxConfigDx {
58 fn default(self, default_effect: impl AsRef<str>) -> Self;
59 fn default_effect(self, default_effect: impl AsRef<str>) -> Self;
60 fn tooltip(self) -> Self;
61 fn tooltips(self) -> Self;
62 }
63
64 impl HoverFxConfigDx for dioxus_hoverfx_core::HoverFxConfig {
65 fn default(self, default_effect: impl AsRef<str>) -> Self {
66 self.with_default_effect(default_effect)
67 }
68
69 fn default_effect(self, default_effect: impl AsRef<str>) -> Self {
70 self.with_default_effect(default_effect)
71 }
72
73 fn tooltip(self) -> Self {
74 self.with_default_effect(dioxus_hoverfx_core::HoverFxPreset::Tooltip.as_attr())
75 }
76
77 fn tooltips(self) -> Self {
78 self.with_effect(dioxus_hoverfx_core::HoverFxDefinition::from_preset(
79 dioxus_hoverfx_core::HoverFxPreset::Tooltip,
80 ))
81 }
82 }
83}
84
85pub const HOVERFX_INIT_HANDLER: &str = "hoverfx.init";
86pub const HOVERFX_REFRESH_HANDLER: &str = "hoverfx.refresh";
87pub const HOVERFX_RADIUS_HANDLER: &str = "hoverfx.radius";
88pub const HOVERFX_RANGE_HANDLER: &str = "hoverfx.range";
89pub const HOVERFX_SHAPE_HANDLER: &str = "hoverfx.shape";
90pub const HOVERFX_FALLOFF_HANDLER: &str = "hoverfx.falloff";
91pub const HOVERFX_STRENGTH_HANDLER: &str = "hoverfx.strength";
92pub const HOVERFX_THEME_CHANGE_EVENT: &str = dioxus_theme_core::THEME_CHANGE_EVENT;
93
94pub fn hoverfx_component_manifest(
95 config: &HoverFxConfig,
96 policy: &HoverFxRoutePolicy,
97) -> HoverFxManifestFragment {
98 config.manifest_fragment(policy)
99}
100
101pub fn hoverfx_component_explain(
102 config: &HoverFxConfig,
103 policy: &HoverFxRoutePolicy,
104) -> HoverFxExplainReport {
105 config.explain(policy)
106}
107
108pub fn hoverfx_native_integration_hints(
109 config: &HoverFxConfig,
110 policy: &HoverFxRoutePolicy,
111) -> std::collections::BTreeMap<String, String> {
112 let mut hints = dioxus_hoverfx_core::hoverfx_native_port_hints(config, policy);
113 hints.insert(
114 "nativeActions".to_string(),
115 hoverfx_native_package_actions(policy.route.as_deref())
116 .len()
117 .to_string(),
118 );
119 hints.insert(
120 "nativePackage".to_string(),
121 hoverfx_native_compatibility_manifest().package,
122 );
123 hints
124}
125
126const DEFAULT_EFFECT: &str = "spotlight";
127const DEFAULT_RADIUS_MIN: u16 = 80;
128const DEFAULT_RADIUS_MAX: u16 = 640;
129const DEFAULT_RADIUS_STEP: u16 = 10;
130const DEFAULT_RANGE_MIN: u16 = 0;
131const DEFAULT_RANGE_MAX: u16 = 1_200;
132const DEFAULT_RANGE_STEP: u16 = 10;
133const DEFAULT_STRENGTH_PERCENT: u16 = 100;
134const DEFAULT_STRENGTH_MIN: u16 = 0;
135const DEFAULT_STRENGTH_MAX: u16 = 200;
136const DEFAULT_STRENGTH_STEP: u16 = 5;
137const HOVERFX_WALLPAPER_LAYER_STYLE: &str = concat!(
138 "position:absolute!important;",
139 "inset:0!important;",
140 "z-index:0!important;",
141 "display:block!important;",
142 "inline-size:100%!important;",
143 "block-size:100%!important;",
144 "width:100%!important;",
145 "height:100%!important;",
146 "min-inline-size:0!important;",
147 "min-block-size:0!important;",
148 "min-width:0!important;",
149 "min-height:0!important;",
150 "max-inline-size:none!important;",
151 "max-block-size:none!important;",
152 "max-width:none!important;",
153 "max-height:none!important;",
154 "margin:0!important;",
155 "padding:0!important;",
156 "border:0!important;",
157 "box-sizing:border-box!important;",
158 "border-radius:inherit!important;",
159 "overflow:hidden!important;",
160 "pointer-events:none!important;",
161 "contain:layout paint style!important;",
162 "flex:none!important;",
163 "align-self:stretch!important;",
164 "grid-area:1 / 1!important;",
165 "mix-blend-mode:var(--dxh-text-layer-blend-mode,var(--dxh-layer-blend-mode,var(--dxh-blend-mode,normal)))!important;"
166);
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
169pub struct HoverFxThemeTokenInterop {
170 pub change_event: &'static str,
171 pub accent_token: &'static str,
172 pub muted_token: &'static str,
173 pub surface_token: &'static str,
174 pub text_token: &'static str,
175 pub sand_color_token: &'static str,
176 pub sand_highlight_token: &'static str,
177}
178
179pub const fn hoverfx_theme_token_interop() -> HoverFxThemeTokenInterop {
180 HoverFxThemeTokenInterop {
181 change_event: dioxus_theme_core::THEME_CHANGE_EVENT,
182 accent_token: dioxus_theme_core::THEME_TOKEN_ACCENT,
183 muted_token: dioxus_theme_core::THEME_TOKEN_MUTED,
184 surface_token: dioxus_theme_core::THEME_TOKEN_SURFACE,
185 text_token: dioxus_theme_core::THEME_TOKEN_TEXT,
186 sand_color_token: dioxus_theme_core::THEME_TOKEN_SURFACE,
187 sand_highlight_token: dioxus_theme_core::THEME_TOKEN_ACCENT,
188 }
189}
190
191#[derive(Clone, Copy, Debug, Eq, PartialEq)]
192pub enum HoverFxRuntimeMode {
193 BrowserRuntime,
194 StaticFallback,
195}
196
197pub fn hoverfx_runtime_mode(_config: &HoverFxConfig) -> HoverFxRuntimeMode {
198 if cfg!(all(feature = "web", target_arch = "wasm32")) {
199 HoverFxRuntimeMode::BrowserRuntime
200 } else {
201 HoverFxRuntimeMode::StaticFallback
202 }
203}
204
205pub fn hoverfx_native_fallback_config() -> HoverFxConfig {
206 HoverFxConfig::default()
207}
208
209pub fn hoverfx_native_compatibility_manifest() -> dioxus_native_port::VisualCompatibilityManifest {
210 dioxus_native_port::native_port_visual_compatibility_manifest("dioxus-hoverfx")
211 .expect("dioxus-hoverfx visual compatibility manifest is registered")
212}
213
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub enum HoverFxNativeAction {
216 Init,
217 Refresh,
218 SetRadius,
219 SetRange,
220 SetShape,
221 SetFalloff,
222 SetStrength,
223}
224
225impl HoverFxNativeAction {
226 pub const fn as_str(self) -> &'static str {
227 match self {
228 Self::Init => "init",
229 Self::Refresh => "refresh",
230 Self::SetRadius => "set-radius",
231 Self::SetRange => "set-range",
232 Self::SetShape => "set-shape",
233 Self::SetFalloff => "set-falloff",
234 Self::SetStrength => "set-strength",
235 }
236 }
237
238 pub const fn handler(self) -> &'static str {
239 match self {
240 Self::Init => HOVERFX_INIT_HANDLER,
241 Self::Refresh => HOVERFX_REFRESH_HANDLER,
242 Self::SetRadius => HOVERFX_RADIUS_HANDLER,
243 Self::SetRange => HOVERFX_RANGE_HANDLER,
244 Self::SetShape => HOVERFX_SHAPE_HANDLER,
245 Self::SetFalloff => HOVERFX_FALLOFF_HANDLER,
246 Self::SetStrength => HOVERFX_STRENGTH_HANDLER,
247 }
248 }
249
250 pub const fn label(self) -> &'static str {
251 match self {
252 Self::Init => "Initialize HoverFX",
253 Self::Refresh => "Refresh HoverFX targets",
254 Self::SetRadius => "Set hover radius",
255 Self::SetRange => "Set hover activation range",
256 Self::SetShape => "Set hover shape",
257 Self::SetFalloff => "Set hover falloff",
258 Self::SetStrength => "Set hover strength",
259 }
260 }
261}
262
263pub fn hoverfx_native_package_actions(
264 route: Option<&str>,
265) -> Vec<dioxus_native_port::NativePackageAction> {
266 let route = route.map(str::to_string);
267 [
268 HoverFxNativeAction::Init,
269 HoverFxNativeAction::Refresh,
270 HoverFxNativeAction::SetRadius,
271 HoverFxNativeAction::SetRange,
272 HoverFxNativeAction::SetShape,
273 HoverFxNativeAction::SetFalloff,
274 HoverFxNativeAction::SetStrength,
275 ]
276 .into_iter()
277 .map(|action| {
278 let mut package_action = dioxus_native_port::NativePackageAction::new(
279 "dioxus-hoverfx",
280 action.as_str(),
281 action.label(),
282 dioxus_native_port::NativeActionKind::NativeAction,
283 )
284 .description(format!(
285 "Controls worker-first cursor hover effects without page hydration. Handler: {}.",
286 action.handler()
287 ));
288 if let Some(route) = route.clone() {
289 package_action = package_action.route(route);
290 }
291 package_action
292 })
293 .collect()
294}
295
296pub fn hoverfx_native_action(
297 config: &HoverFxConfig,
298 action: HoverFxNativeAction,
299) -> dioxus_native_port::NativeActionResult {
300 let mode = hoverfx_runtime_mode(config);
301 let backend = match mode {
302 HoverFxRuntimeMode::BrowserRuntime => "browser-runtime",
303 HoverFxRuntimeMode::StaticFallback => "static-fallback",
304 };
305
306 dioxus_native_port::NativeActionResult::succeeded(
307 "dioxus-hoverfx",
308 action.as_str(),
309 dioxus_native_port::NativeActionKind::NativeAction,
310 format!("{} prepared", action.label()),
311 )
312 .with_backend(backend)
313 .with_output("handler", action.handler())
314}
315
316pub fn hoverfx_preset_attr(preset: HoverFxPreset) -> &'static str {
317 preset.as_attr()
318}
319
320pub fn hoverfx_preset_label(preset: HoverFxPreset) -> &'static str {
321 preset.label()
322}
323
324pub fn hoverfx_shape_attr(shape: HoverFxShape) -> &'static str {
325 shape.as_attr()
326}
327
328pub fn hoverfx_shape_label(shape: HoverFxShape) -> &'static str {
329 shape.label()
330}
331
332pub fn hoverfx_falloff_attr(falloff: HoverFxFalloff) -> &'static str {
333 falloff.as_attr()
334}
335
336pub fn hoverfx_falloff_label(falloff: HoverFxFalloff) -> &'static str {
337 falloff.label()
338}
339
340pub fn hoverfx_text_contrast_attr(text_contrast: HoverFxTextContrastMode) -> &'static str {
341 text_contrast.as_attr()
342}
343
344fn hoverfx_control_id(prefix: &str, handler: &str, label: &str) -> String {
345 format!("{prefix}-{}", hoverfx_id(format!("{handler}-{label}")))
346}
347
348fn sanitized_effect(effect: &str, preset: Option<HoverFxPreset>) -> String {
349 preset
350 .map(|preset| hoverfx_preset_attr(preset).to_string())
351 .unwrap_or_else(|| hoverfx_id(effect))
352}
353
354fn optional_u16_attr(value: Option<u16>) -> String {
355 value.map(|value| value.to_string()).unwrap_or_default()
356}
357
358fn optional_f32_attr(value: Option<f32>) -> String {
359 value.map(format_float).unwrap_or_default()
360}
361
362fn bool_attr(value: bool) -> &'static str {
363 if value { "true" } else { "false" }
364}
365
366fn format_float(value: f32) -> String {
367 let mut formatted = format!("{value:.3}");
368 while formatted.contains('.') && formatted.ends_with('0') {
369 formatted.pop();
370 }
371 if formatted.ends_with('.') {
372 formatted.pop();
373 }
374 formatted
375}
376
377fn optional_shape_attr(value: Option<HoverFxShape>) -> String {
378 value
379 .map(hoverfx_shape_attr)
380 .unwrap_or_default()
381 .to_string()
382}
383
384fn optional_falloff_attr(value: Option<HoverFxFalloff>) -> String {
385 value
386 .map(hoverfx_falloff_attr)
387 .unwrap_or_default()
388 .to_string()
389}
390
391fn optional_text_reveal_attr(value: Option<&HoverFxTextRevealConfig>) -> String {
392 value
393 .and_then(|value| value.to_json().ok())
394 .unwrap_or_default()
395}
396
397fn optional_texture_reveal_attr(value: Option<&HoverFxTextureRevealConfig>) -> String {
398 value
399 .and_then(|value| value.to_json().ok())
400 .unwrap_or_default()
401}
402
403fn optional_sand_attr(value: Option<&HoverFxSandConfig>) -> String {
404 value
405 .and_then(|value| value.to_json().ok())
406 .unwrap_or_default()
407}
408
409fn optional_tooltip_attr(value: Option<&HoverFxTooltipConfig>) -> String {
410 value
411 .and_then(|value| value.to_json().ok())
412 .unwrap_or_default()
413}
414
415fn optional_tooltip_text_attr(value: Option<&HoverFxTooltipConfig>, fallback: &str) -> String {
416 value
417 .map(|value| value.text.as_str())
418 .filter(|text| !text.trim().is_empty())
419 .unwrap_or(fallback)
420 .to_string()
421}
422
423fn optional_tooltip_i18n_key_attr(value: Option<&HoverFxTooltipConfig>, fallback: &str) -> String {
424 value
425 .and_then(|value| value.i18n_key.as_deref())
426 .filter(|key| !key.trim().is_empty())
427 .unwrap_or(fallback)
428 .to_string()
429}
430
431fn optional_text_contrast_attr(value: Option<HoverFxTextContrastMode>) -> String {
432 value
433 .map(hoverfx_text_contrast_attr)
434 .unwrap_or_default()
435 .to_string()
436}
437
438fn optional_textfx_config_attr(
439 value: Option<&HoverFxTextRevealConfig>,
440 interop: bool,
441 id: &str,
442) -> String {
443 if !interop {
444 return String::new();
445 }
446 let Some(value) = value else {
447 return String::new();
448 };
449 if value.animation_source == HoverFxTextAnimationSource::HoverFx {
450 return String::new();
451 }
452 value
453 .to_textfx_config_json(
454 if id.trim().is_empty() {
455 "hoverfx-binary-reveal"
456 } else {
457 id
458 },
459 "010101001101",
460 )
461 .unwrap_or_default()
462}
463
464#[derive(Props, Clone, PartialEq)]
465pub struct HoverFxProviderProps {
466 #[props(default)]
467 pub config: HoverFxConfig,
468 #[props(default)]
469 pub class: String,
470 #[props(default = "document".to_string())]
471 pub scope: String,
472 #[props(default = true)]
473 pub auto_init: bool,
474 pub children: Element,
475}
476
477#[component]
478pub fn HoverFxProvider(props: HoverFxProviderProps) -> Element {
479 let mode = hoverfx_runtime_mode(&props.config);
480 let runtime = match mode {
481 HoverFxRuntimeMode::BrowserRuntime => "browser-runtime",
482 HoverFxRuntimeMode::StaticFallback => "static-fallback",
483 };
484 let auto_init = if props.auto_init { "true" } else { "false" };
485 let default_effect = hoverfx_id(&props.config.default_effect);
486 let radius = props.config.radius_px.to_string();
487 let range = props.config.range_px.to_string();
488 let shape = props.config.shape.as_attr();
489 let falloff = props.config.falloff.as_attr();
490 let strength = format_float(props.config.strength);
491 let smoothing = format_float(props.config.smoothing);
492 let max_active = props.config.max_active_elements.to_string();
493 let renderer = props.config.renderer.as_attr();
494 let runtime_path = props.config.runtime_path.clone();
495 let worker_path = props.config.worker_path.clone();
496 let theme_tokens = hoverfx_theme_token_interop();
497 rsx! {
498 div {
499 class: "{props.class}",
500 "data-dxh-provider": "true",
501 "data-dxh-scope": "{props.scope}",
502 "data-dxh-theme-event": "{theme_tokens.change_event}",
503 "data-dxh-theme-accent-token": "{theme_tokens.accent_token}",
504 "data-dxh-theme-muted-token": "{theme_tokens.muted_token}",
505 "data-dxh-theme-surface-token": "{theme_tokens.surface_token}",
506 "data-dxh-theme-text-token": "{theme_tokens.text_token}",
507 "data-dxh-theme-sand-color-token": "{theme_tokens.sand_color_token}",
508 "data-dxh-theme-sand-highlight-token": "{theme_tokens.sand_highlight_token}",
509 "data-dxh-runtime": "{runtime}",
510 "data-dxh-renderer": "{renderer}",
511 "data-dxh-auto-init": "{auto_init}",
512 "data-dxh-default-effect": "{default_effect}",
513 "data-dxh-radius": "{radius}",
514 "data-dxh-range": "{range}",
515 "data-dxh-shape": "{shape}",
516 "data-dxh-falloff": "{falloff}",
517 "data-dxh-strength": "{strength}",
518 "data-dxh-smoothing": "{smoothing}",
519 "data-dxh-max-active-elements": "{max_active}",
520 "data-dxh-runtime-path": "{runtime_path}",
521 "data-dxh-worker-path": "{worker_path}",
522 {props.children}
523 }
524 }
525}
526
527#[derive(Props, Clone, PartialEq)]
528pub struct HoverFxTargetProps {
529 #[props(default = DEFAULT_EFFECT.to_string())]
530 pub effect: String,
531 #[props(default)]
532 pub preset: Option<HoverFxPreset>,
533 #[props(default)]
534 pub radius: Option<u16>,
535 #[props(default)]
536 pub range: Option<u16>,
537 #[props(default)]
538 pub shape: Option<HoverFxShape>,
539 #[props(default)]
540 pub falloff: Option<HoverFxFalloff>,
541 #[props(default)]
542 pub strength: Option<f32>,
543 #[props(default)]
544 pub contained: bool,
545 #[props(default)]
546 pub controlled: bool,
547 #[props(default)]
548 pub text_reveal: Option<HoverFxTextRevealConfig>,
549 #[props(default)]
550 pub texture_reveal: Option<HoverFxTextureRevealConfig>,
551 #[props(default)]
552 pub sand: Option<HoverFxSandConfig>,
553 #[props(default)]
554 pub tooltip: Option<HoverFxTooltipConfig>,
555 #[props(default)]
556 pub tooltip_text: String,
557 #[props(default)]
558 pub tooltip_i18n_key: String,
559 #[props(default)]
560 pub text_contrast: Option<HoverFxTextContrastMode>,
561 #[props(default)]
562 pub textfx_interop: bool,
563 #[props(default)]
564 pub class: String,
565 #[props(default)]
566 pub id: String,
567 pub children: Element,
568}
569
570#[component]
571pub fn HoverFxTarget(props: HoverFxTargetProps) -> Element {
572 let effect = sanitized_effect(&props.effect, props.preset);
573 let radius = optional_u16_attr(props.radius);
574 let range = optional_u16_attr(props.range);
575 let shape = optional_shape_attr(props.shape);
576 let falloff = optional_falloff_attr(props.falloff);
577 let strength = optional_f32_attr(props.strength);
578 let contained = bool_attr(props.contained);
579 let controlled = bool_attr(props.controlled);
580 let text_reveal = optional_text_reveal_attr(props.text_reveal.as_ref());
581 let texture_reveal = optional_texture_reveal_attr(props.texture_reveal.as_ref());
582 let sand = optional_sand_attr(props.sand.as_ref());
583 let tooltip = optional_tooltip_attr(props.tooltip.as_ref());
584 let tooltip_text = optional_tooltip_text_attr(props.tooltip.as_ref(), &props.tooltip_text);
585 let tooltip_i18n_key =
586 optional_tooltip_i18n_key_attr(props.tooltip.as_ref(), &props.tooltip_i18n_key);
587 let text_contrast = optional_text_contrast_attr(props.text_contrast);
588 let textfx_config =
589 optional_textfx_config_attr(props.text_reveal.as_ref(), props.textfx_interop, &props.id);
590 let wallpaper_layer_style = HOVERFX_WALLPAPER_LAYER_STYLE;
591 let needs_textfx_handler = !textfx_config.is_empty()
592 || !tooltip.is_empty()
593 || !tooltip_text.is_empty()
594 || !tooltip_i18n_key.is_empty();
595
596 if textfx_config.is_empty() && !needs_textfx_handler {
597 rsx! {
598 div {
599 id: "{props.id}",
600 class: "{props.class}",
601 "data-dxh-target": "true",
602 "data-dxh-effect": "{effect}",
603 "data-dxh-radius": "{radius}",
604 "data-dxh-range": "{range}",
605 "data-dxh-shape": "{shape}",
606 "data-dxh-falloff": "{falloff}",
607 "data-dxh-strength": "{strength}",
608 "data-dxh-contain": "{contained}",
609 "data-dxh-controlled": "{controlled}",
610 "data-dxh-text-reveal": "{text_reveal}",
611 "data-dxh-texture-reveal": "{texture_reveal}",
612 "data-dxh-sand": "{sand}",
613 "data-dxh-tooltip": "{tooltip_text}",
614 "data-dxh-tooltip-i18n-key": "{tooltip_i18n_key}",
615 "data-dxh-tooltip-config": "{tooltip}",
616 "data-dxh-text-contrast": "{text_contrast}",
617 {props.children}
618 }
619 }
620 } else if textfx_config.is_empty() {
621 rsx! {
622 div {
623 id: "{props.id}",
624 class: "{props.class}",
625 "data-dxh-target": "true",
626 "data-dxh-effect": "{effect}",
627 "data-dxh-radius": "{radius}",
628 "data-dxh-range": "{range}",
629 "data-dxh-shape": "{shape}",
630 "data-dxh-falloff": "{falloff}",
631 "data-dxh-strength": "{strength}",
632 "data-dxh-contain": "{contained}",
633 "data-dxh-controlled": "{controlled}",
634 "data-dxh-text-reveal": "{text_reveal}",
635 "data-dxh-texture-reveal": "{texture_reveal}",
636 "data-dxh-sand": "{sand}",
637 "data-dxh-tooltip": "{tooltip_text}",
638 "data-dxh-tooltip-i18n-key": "{tooltip_i18n_key}",
639 "data-dxh-tooltip-config": "{tooltip}",
640 "data-dxh-text-contrast": "{text_contrast}",
641 "data-dxr-on-pointerover": "textfx.run",
642 {props.children}
643 }
644 }
645 } else {
646 rsx! {
647 div {
648 id: "{props.id}",
649 class: "{props.class}",
650 "data-dxh-target": "true",
651 "data-dxh-effect": "{effect}",
652 "data-dxh-radius": "{radius}",
653 "data-dxh-range": "{range}",
654 "data-dxh-shape": "{shape}",
655 "data-dxh-falloff": "{falloff}",
656 "data-dxh-strength": "{strength}",
657 "data-dxh-contain": "{contained}",
658 "data-dxh-controlled": "{controlled}",
659 "data-dxh-text-reveal": "{text_reveal}",
660 "data-dxh-texture-reveal": "{texture_reveal}",
661 "data-dxh-sand": "{sand}",
662 "data-dxh-tooltip": "{tooltip_text}",
663 "data-dxh-tooltip-i18n-key": "{tooltip_i18n_key}",
664 "data-dxh-tooltip-config": "{tooltip}",
665 "data-dxh-text-contrast": "{text_contrast}",
666 "data-dxr-on-pointerover": "textfx.run",
667 span {
668 class: "dxh-text-reveal-layer",
669 style: "{wallpaper_layer_style}",
670 "aria-hidden": "true",
671 "data-dxh-text-layer": "true",
672 "data-dxt-textfx": "{textfx_config}"
673 }
674 {props.children}
675 }
676 }
677 }
678}
679
680#[derive(Props, Clone, PartialEq)]
681pub struct HoverFxCardProps {
682 #[props(default = DEFAULT_EFFECT.to_string())]
683 pub effect: String,
684 #[props(default)]
685 pub preset: Option<HoverFxPreset>,
686 #[props(default)]
687 pub radius: Option<u16>,
688 #[props(default)]
689 pub range: Option<u16>,
690 #[props(default)]
691 pub shape: Option<HoverFxShape>,
692 #[props(default)]
693 pub falloff: Option<HoverFxFalloff>,
694 #[props(default)]
695 pub strength: Option<f32>,
696 #[props(default)]
697 pub contained: bool,
698 #[props(default)]
699 pub controlled: bool,
700 #[props(default)]
701 pub text_reveal: Option<HoverFxTextRevealConfig>,
702 #[props(default)]
703 pub texture_reveal: Option<HoverFxTextureRevealConfig>,
704 #[props(default)]
705 pub sand: Option<HoverFxSandConfig>,
706 #[props(default)]
707 pub tooltip: Option<HoverFxTooltipConfig>,
708 #[props(default)]
709 pub tooltip_text: String,
710 #[props(default)]
711 pub tooltip_i18n_key: String,
712 #[props(default)]
713 pub text_contrast: Option<HoverFxTextContrastMode>,
714 #[props(default)]
715 pub textfx_interop: bool,
716 #[props(default)]
717 pub class: String,
718 #[props(default)]
719 pub id: String,
720 pub children: Element,
721}
722
723#[component]
724pub fn HoverFxCard(props: HoverFxCardProps) -> Element {
725 rsx! {
726 HoverFxTarget {
727 id: props.id,
728 class: props.class,
729 effect: props.effect,
730 preset: props.preset,
731 radius: props.radius,
732 range: props.range,
733 shape: props.shape,
734 falloff: props.falloff,
735 strength: props.strength,
736 contained: props.contained,
737 controlled: props.controlled,
738 text_reveal: props.text_reveal,
739 texture_reveal: props.texture_reveal,
740 sand: props.sand,
741 tooltip: props.tooltip,
742 tooltip_text: props.tooltip_text,
743 tooltip_i18n_key: props.tooltip_i18n_key,
744 text_contrast: props.text_contrast,
745 textfx_interop: props.textfx_interop,
746 div {
747 "data-dxh-card": "true",
748 {props.children}
749 }
750 }
751 }
752}
753
754#[derive(Props, Clone, PartialEq)]
755pub struct HoverFxRadiusSliderProps {
756 #[props(default = HOVERFX_RADIUS_HANDLER.to_string())]
757 pub handler: String,
758 #[props(default)]
759 pub class: String,
760 #[props(default = "Hover radius".to_string())]
761 pub label: String,
762 #[props(default = DEFAULT_HOVERFX_RADIUS_PX)]
763 pub value: u16,
764 #[props(default = DEFAULT_RADIUS_MIN)]
765 pub min: u16,
766 #[props(default = DEFAULT_RADIUS_MAX)]
767 pub max: u16,
768 #[props(default = DEFAULT_RADIUS_STEP)]
769 pub step: u16,
770 #[props(default)]
771 pub apply_to_all: bool,
772}
773
774#[component]
775pub fn HoverFxRadiusSlider(props: HoverFxRadiusSliderProps) -> Element {
776 let input_id = hoverfx_control_id("dxh-radius", &props.handler, &props.label);
777 let output_id = format!("{input_id}-output");
778 let min = props.min.min(props.max);
779 let max = props.max.max(props.min);
780 let value = props.value.clamp(min, max);
781 let step = props.step.max(1);
782 let apply_to = if props.apply_to_all { "all" } else { "target" };
783 rsx! {
784 label {
785 class: "{props.class}",
786 "data-dxh-control": "radius",
787 "data-dxh-apply-to": "{apply_to}",
788 "for": "{input_id}",
789 span {
790 "{props.label}: "
791 output {
792 id: "{output_id}",
793 "for": "{input_id}",
794 "aria-live": "polite",
795 "data-dxh-radius-current": "true",
796 "{value}px"
797 }
798 }
799 input {
800 id: "{input_id}",
801 r#type: "range",
802 min: "{min}",
803 max: "{max}",
804 step: "{step}",
805 value: "{value}",
806 "aria-describedby": "{output_id}",
807 "data-dxr-on-input": "{props.handler}",
808 "data-dxh-radius-control": "true"
809 }
810 }
811 }
812}
813
814#[derive(Props, Clone, PartialEq)]
815pub struct HoverFxRangeSliderProps {
816 #[props(default = HOVERFX_RANGE_HANDLER.to_string())]
817 pub handler: String,
818 #[props(default)]
819 pub class: String,
820 #[props(default = "Hover range".to_string())]
821 pub label: String,
822 #[props(default = DEFAULT_HOVERFX_RANGE_PX)]
823 pub value: u16,
824 #[props(default = DEFAULT_RANGE_MIN)]
825 pub min: u16,
826 #[props(default = DEFAULT_RANGE_MAX)]
827 pub max: u16,
828 #[props(default = DEFAULT_RANGE_STEP)]
829 pub step: u16,
830 #[props(default = true)]
831 pub apply_to_all: bool,
832}
833
834#[component]
835pub fn HoverFxRangeSlider(props: HoverFxRangeSliderProps) -> Element {
836 let input_id = hoverfx_control_id("dxh-range", &props.handler, &props.label);
837 let output_id = format!("{input_id}-output");
838 let min = props.min.min(props.max);
839 let max = props.max.max(props.min);
840 let value = props.value.clamp(min, max);
841 let step = props.step.max(1);
842 let apply_to = if props.apply_to_all { "all" } else { "target" };
843 rsx! {
844 label {
845 class: "{props.class}",
846 "data-dxh-control": "range",
847 "data-dxh-apply-to": "{apply_to}",
848 "for": "{input_id}",
849 span {
850 "{props.label}: "
851 output {
852 id: "{output_id}",
853 "for": "{input_id}",
854 "aria-live": "polite",
855 "data-dxh-range-current": "true",
856 "{value}px"
857 }
858 }
859 input {
860 id: "{input_id}",
861 r#type: "range",
862 min: "{min}",
863 max: "{max}",
864 step: "{step}",
865 value: "{value}",
866 "aria-describedby": "{output_id}",
867 "data-dxr-on-input": "{props.handler}",
868 "data-dxh-range-control": "true"
869 }
870 }
871 }
872}
873
874#[derive(Props, Clone, PartialEq)]
875pub struct HoverFxShapeSelectProps {
876 #[props(default = HOVERFX_SHAPE_HANDLER.to_string())]
877 pub handler: String,
878 #[props(default)]
879 pub class: String,
880 #[props(default = "Hover shape".to_string())]
881 pub label: String,
882 #[props(default = HoverFxShape::Circle)]
883 pub value: HoverFxShape,
884 #[props(default)]
885 pub apply_to_all: bool,
886}
887
888#[component]
889pub fn HoverFxShapeSelect(props: HoverFxShapeSelectProps) -> Element {
890 let select_id = hoverfx_control_id("dxh-shape", &props.handler, &props.label);
891 let label_id = format!("{select_id}-label");
892 let current_id = format!("{select_id}-current");
893 let current = hoverfx_shape_attr(props.value);
894 let current_label = hoverfx_shape_label(props.value);
895 let apply_to = if props.apply_to_all { "all" } else { "target" };
896 rsx! {
897 label {
898 class: "{props.class}",
899 "data-dxh-control": "shape",
900 "data-dxh-apply-to": "{apply_to}",
901 "for": "{select_id}",
902 span {
903 id: "{label_id}",
904 "{props.label}"
905 }
906 select {
907 id: "{select_id}",
908 value: "{current}",
909 "aria-labelledby": "{label_id} {current_id}",
910 "data-dxr-on-change": "{props.handler}",
911 "data-dxh-shape-control": "true",
912 for shape in [
913 HoverFxShape::Circle,
914 HoverFxShape::Square,
915 HoverFxShape::RoundedRect,
916 HoverFxShape::Polygon,
917 ] {
918 option {
919 value: "{hoverfx_shape_attr(shape)}",
920 selected: shape == props.value,
921 "{hoverfx_shape_label(shape)}"
922 }
923 }
924 }
925 span {
926 id: "{current_id}",
927 "aria-live": "polite",
928 "data-dxh-shape-current": "true",
929 "{current_label}"
930 }
931 }
932 }
933}
934
935#[derive(Props, Clone, PartialEq)]
936pub struct HoverFxFalloffSelectProps {
937 #[props(default = HOVERFX_FALLOFF_HANDLER.to_string())]
938 pub handler: String,
939 #[props(default)]
940 pub class: String,
941 #[props(default = "Hover falloff".to_string())]
942 pub label: String,
943 #[props(default = HoverFxFalloff::Smooth)]
944 pub value: HoverFxFalloff,
945 #[props(default)]
946 pub apply_to_all: bool,
947}
948
949#[component]
950pub fn HoverFxFalloffSelect(props: HoverFxFalloffSelectProps) -> Element {
951 let select_id = hoverfx_control_id("dxh-falloff", &props.handler, &props.label);
952 let label_id = format!("{select_id}-label");
953 let current_id = format!("{select_id}-current");
954 let current = hoverfx_falloff_attr(props.value);
955 let current_label = hoverfx_falloff_label(props.value);
956 let apply_to = if props.apply_to_all { "all" } else { "target" };
957 rsx! {
958 label {
959 class: "{props.class}",
960 "data-dxh-control": "falloff",
961 "data-dxh-apply-to": "{apply_to}",
962 "for": "{select_id}",
963 span {
964 id: "{label_id}",
965 "{props.label}"
966 }
967 select {
968 id: "{select_id}",
969 value: "{current}",
970 "aria-labelledby": "{label_id} {current_id}",
971 "data-dxr-on-change": "{props.handler}",
972 "data-dxh-falloff-control": "true",
973 for falloff in [
974 HoverFxFalloff::Hard,
975 HoverFxFalloff::Linear,
976 HoverFxFalloff::Smooth,
977 HoverFxFalloff::Exponential,
978 ] {
979 option {
980 value: "{hoverfx_falloff_attr(falloff)}",
981 selected: falloff == props.value,
982 "{hoverfx_falloff_label(falloff)}"
983 }
984 }
985 }
986 span {
987 id: "{current_id}",
988 "aria-live": "polite",
989 "data-dxh-falloff-current": "true",
990 "{current_label}"
991 }
992 }
993 }
994}
995
996#[derive(Props, Clone, PartialEq)]
997pub struct HoverFxStrengthSliderProps {
998 #[props(default = HOVERFX_STRENGTH_HANDLER.to_string())]
999 pub handler: String,
1000 #[props(default)]
1001 pub class: String,
1002 #[props(default = "Hover strength".to_string())]
1003 pub label: String,
1004 #[props(default = DEFAULT_STRENGTH_PERCENT)]
1005 pub value: u16,
1006 #[props(default = DEFAULT_STRENGTH_MIN)]
1007 pub min: u16,
1008 #[props(default = DEFAULT_STRENGTH_MAX)]
1009 pub max: u16,
1010 #[props(default = DEFAULT_STRENGTH_STEP)]
1011 pub step: u16,
1012 #[props(default)]
1013 pub apply_to_all: bool,
1014}
1015
1016#[component]
1017pub fn HoverFxStrengthSlider(props: HoverFxStrengthSliderProps) -> Element {
1018 let input_id = hoverfx_control_id("dxh-strength", &props.handler, &props.label);
1019 let output_id = format!("{input_id}-output");
1020 let min = props.min.min(props.max);
1021 let max = props.max.max(props.min);
1022 let value = props.value.clamp(min, max);
1023 let step = props.step.max(1);
1024 let apply_to = if props.apply_to_all { "all" } else { "target" };
1025 rsx! {
1026 label {
1027 class: "{props.class}",
1028 "data-dxh-control": "strength",
1029 "data-dxh-apply-to": "{apply_to}",
1030 "for": "{input_id}",
1031 span {
1032 "{props.label}: "
1033 output {
1034 id: "{output_id}",
1035 "for": "{input_id}",
1036 "aria-live": "polite",
1037 "data-dxh-strength-current": "true",
1038 "{value}%"
1039 }
1040 }
1041 input {
1042 id: "{input_id}",
1043 r#type: "range",
1044 min: "{min}",
1045 max: "{max}",
1046 step: "{step}",
1047 value: "{value}",
1048 "aria-describedby": "{output_id}",
1049 "data-dxr-on-input": "{props.handler}",
1050 "data-dxh-strength-control": "true"
1051 }
1052 }
1053 }
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058 use super::*;
1059
1060 #[test]
1061 fn native_actions_include_expected_handlers() {
1062 let actions = hoverfx_native_package_actions(Some("/hoverfx"));
1063 let manifest = hoverfx_native_compatibility_manifest();
1064 assert_eq!(manifest.package, "dioxus-hoverfx");
1065 assert!(actions.iter().any(|action| action.action == "init"));
1066 assert!(actions.iter().any(|action| action.action == "refresh"));
1067 assert!(actions.iter().any(|action| action.action == "set-radius"));
1068 assert!(actions.iter().any(|action| action.action == "set-range"));
1069 assert!(actions.iter().any(|action| action.action == "set-shape"));
1070 assert!(actions.iter().any(|action| action.action == "set-falloff"));
1071 assert!(actions.iter().any(|action| action.action == "set-strength"));
1072 assert!(
1073 actions
1074 .iter()
1075 .any(|action| action.description.contains(HOVERFX_RADIUS_HANDLER))
1076 );
1077 assert!(
1078 actions
1079 .iter()
1080 .any(|action| action.description.contains(HOVERFX_RANGE_HANDLER))
1081 );
1082 }
1083
1084 #[test]
1085 fn helpers_emit_runtime_attribute_values() {
1086 assert_eq!(hoverfx_preset_attr(HoverFxPreset::SoftGlow), "soft-glow");
1087 assert_eq!(
1088 hoverfx_preset_attr(HoverFxPreset::BinaryReveal),
1089 "binary-reveal"
1090 );
1091 assert_eq!(
1092 hoverfx_preset_attr(HoverFxPreset::TextureReveal),
1093 "texture-reveal"
1094 );
1095 assert_eq!(hoverfx_preset_attr(HoverFxPreset::Sand), "sand");
1096 assert_eq!(
1097 hoverfx_shape_attr(HoverFxShape::RoundedRect),
1098 "rounded-rect"
1099 );
1100 assert_eq!(
1101 hoverfx_falloff_attr(HoverFxFalloff::Exponential),
1102 "exponential"
1103 );
1104 assert_eq!(
1105 hoverfx_text_contrast_attr(HoverFxTextContrastMode::Invert),
1106 "invert"
1107 );
1108 }
1109
1110 #[test]
1111 fn text_reveal_attrs_emit_hoverfx_and_optional_textfx_config() {
1112 let config = HoverFxTextRevealConfig::default();
1113 let hoverfx = optional_text_reveal_attr(Some(&config));
1114 assert!(hoverfx.contains(r#""charset":"01""#));
1115 assert!(hoverfx.contains(r#""animationSource":"auto""#));
1116 assert!(hoverfx.contains(r#""renderer":"glyph-atlas""#));
1117
1118 let textfx = optional_textfx_config_attr(Some(&config), true, "binary-card");
1119 assert!(textfx.contains(r#""effect":"scramble""#));
1120 assert!(textfx.contains(r#""charset":"01""#));
1121 assert!(textfx.contains(r#""speedMs":220"#));
1122 assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("position:absolute!important"));
1123 assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("inset:0!important"));
1124 assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("flex:none!important"));
1125 assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("grid-area:1 / 1!important"));
1126 assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("max-width:none!important"));
1127 assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("max-height:none!important"));
1128 assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("pointer-events:none!important"));
1129 assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("contain:layout paint style!important"));
1130 assert!(HOVERFX_WALLPAPER_LAYER_STYLE.contains("mix-blend-mode:var("));
1131 assert!(optional_textfx_config_attr(Some(&config), false, "binary-card").is_empty());
1132 assert!(
1133 optional_textfx_config_attr(
1134 Some(
1135 &config
1136 .clone()
1137 .with_animation_source(HoverFxTextAnimationSource::HoverFx)
1138 ),
1139 true,
1140 "binary-card"
1141 )
1142 .is_empty()
1143 );
1144 }
1145
1146 #[test]
1147 fn texture_reveal_attr_emits_hoverfx_config() {
1148 let config =
1149 HoverFxTextureRevealConfig::default().with_mode(HoverFxTextureRevealMode::Halftone);
1150 let hoverfx = optional_texture_reveal_attr(Some(&config));
1151 assert!(hoverfx.contains(r#""mode":"halftone""#));
1152 assert!(optional_texture_reveal_attr(None).is_empty());
1153 }
1154
1155 #[test]
1156 fn sand_attr_emits_hoverfx_config() {
1157 let config = HoverFxSandConfig::default()
1158 .with_grain_size_px(1.4)
1159 .with_shimmer_strength(0.9)
1160 .with_shimmer_radius_px(280.0)
1161 .with_specular_strength(1.2)
1162 .with_color_source(HoverFxSandColorSource::Element)
1163 .with_animation_speed_ms(720);
1164 let hoverfx = optional_sand_attr(Some(&config));
1165 assert!(hoverfx.contains(r#""grainSizePx":"#));
1166 assert!(hoverfx.contains(r#""shimmerStrength":"#));
1167 assert!(hoverfx.contains(r#""shimmerRadiusPx":"#));
1168 assert!(hoverfx.contains(r#""specularStrength":"#));
1169 assert!(hoverfx.contains(r#""colorSource":"element""#));
1170 assert!(hoverfx.contains(r#""animationSpeedMs":720"#));
1171 assert!(optional_sand_attr(None).is_empty());
1172 }
1173
1174 #[test]
1175 fn text_contrast_attr_emits_mode() {
1176 assert_eq!(
1177 optional_text_contrast_attr(Some(HoverFxTextContrastMode::Auto)),
1178 "auto"
1179 );
1180 assert_eq!(
1181 optional_text_contrast_attr(Some(HoverFxTextContrastMode::Darken)),
1182 "darken"
1183 );
1184 assert_eq!(
1185 optional_text_contrast_attr(Some(HoverFxTextContrastMode::Invert)),
1186 "invert"
1187 );
1188 assert!(optional_text_contrast_attr(None).is_empty());
1189 }
1190
1191 #[test]
1192 fn theme_token_interop_metadata_uses_shared_contract() {
1193 let interop = hoverfx_theme_token_interop();
1194 assert_eq!(interop.change_event, "dioxus-theme:change");
1195 assert_eq!(interop.accent_token, dioxus_theme_core::THEME_TOKEN_ACCENT);
1196 assert_eq!(
1197 interop.surface_token,
1198 dioxus_theme_core::THEME_TOKEN_SURFACE
1199 );
1200 assert_eq!(
1201 interop.sand_color_token,
1202 dioxus_theme_core::THEME_TOKEN_SURFACE
1203 );
1204 assert_eq!(
1205 interop.sand_highlight_token,
1206 dioxus_theme_core::THEME_TOKEN_ACCENT
1207 );
1208 }
1209
1210 #[test]
1211 fn native_action_reports_handler() {
1212 let result =
1213 hoverfx_native_action(&HoverFxConfig::default(), HoverFxNativeAction::SetRadius);
1214 assert_eq!(
1215 result.outputs.get("handler"),
1216 Some(&HOVERFX_RADIUS_HANDLER.to_string())
1217 );
1218 }
1219
1220 #[test]
1221 fn dx_hoverfx_syntax_builds_worker_first_config() {
1222 use crate::dx::HoverFxConfigDx;
1223
1224 let config = crate::dx::hoverfx()
1225 .default("glass")
1226 .radius(24)
1227 .range(96)
1228 .worker()
1229 .lazy_layers();
1230
1231 assert_eq!(config.default_effect, "glass");
1232 assert_eq!(config.radius_px, 24);
1233 assert_eq!(config.range_px, 96);
1234 assert_eq!(config.renderer, HoverFxRenderer::WorkerFirst);
1235 assert!(config.performance.lazy_local_layers);
1236 }
1237
1238 #[test]
1239 fn dx_tooltip_syntax_builds_textfx_tooltip_config() {
1240 use crate::dx::HoverFxConfigDx;
1241
1242 let config = crate::dx::hoverfx().tooltips().tooltip().lazy_layers();
1243 let tooltip = crate::dx::tooltip("Explain this control")
1244 .key("hoverfx.tooltip.text")
1245 .offset(10)
1246 .opacity(0.84)
1247 .scramble()
1248 .dur_ms(220);
1249 let keyed = crate::dx::tooltip_key("hoverfx.tooltip.text", "Explain this control");
1250
1251 assert_eq!(config.default_effect, HoverFxPreset::Tooltip.as_attr());
1252 assert!(
1253 config
1254 .registry
1255 .contains_effect(HoverFxPreset::Tooltip.as_attr())
1256 );
1257 assert_eq!(tooltip.text, "Explain this control");
1258 assert_eq!(tooltip.i18n_key.as_deref(), Some("hoverfx.tooltip.text"));
1259 assert_eq!(keyed.i18n_key.as_deref(), Some("hoverfx.tooltip.text"));
1260 assert_eq!(tooltip.placement, HoverFxTooltipPlacement::Cursor);
1261 assert_eq!(tooltip.box_opacity, 0.84);
1262 assert_eq!(tooltip.duration_ms, 220);
1263 assert_eq!(tooltip.textfx_effect, "scramble");
1264 }
1265
1266 #[test]
1267 fn component_manifest_wraps_core_policy_and_native_hints() {
1268 let config = HoverFxConfig::new()
1269 .with_effect(HoverFxDefinition::from_preset(HoverFxPreset::Tooltip));
1270 let policy = hoverfx_route_policy().route("/hoverfx").tag("native");
1271 let manifest = hoverfx_component_manifest(&config, &policy);
1272 let explain = hoverfx_component_explain(&config, &policy);
1273 let hints = hoverfx_native_integration_hints(&config, &policy);
1274
1275 assert_eq!(manifest.route.as_deref(), Some("/hoverfx"));
1276 assert_eq!(explain.manifest.cache_key, manifest.cache_key);
1277 assert_eq!(hints["nativePackage"], "dioxus-hoverfx");
1278 assert!(hints["nativeActions"].parse::<usize>().unwrap() >= 1);
1279 }
1280
1281 #[test]
1282 fn tooltip_target_attrs_serialize_for_runtime_and_textfx() {
1283 let tooltip = HoverFxTooltipConfig::new("Use TextFX for the label")
1284 .key("hoverfx.tooltip.text")
1285 .cursor()
1286 .typewriter();
1287 let text = optional_tooltip_text_attr(Some(&tooltip), "");
1288 let key = optional_tooltip_i18n_key_attr(Some(&tooltip), "");
1289 let config = optional_tooltip_attr(Some(&tooltip));
1290
1291 assert_eq!(text, "Use TextFX for the label");
1292 assert_eq!(key, "hoverfx.tooltip.text");
1293 assert!(config.contains(r#""placement":"cursor""#));
1294 assert!(config.contains(r#""i18nKey":"hoverfx.tooltip.text""#));
1295 assert!(config.contains(r#""boxOpacity":0.88"#));
1296 assert!(config.contains(r#""textfxEffect":"typewriter""#));
1297 assert!(config.contains(r#""durationMs":260"#));
1298 }
1299}