Search code examples
rustlifetimeyewweb-sys

Is there any way to cast a Callback<MouseEvent> into FnMut(MouseEvent)?


I'm implementing material 3 from scratch with web_sys and writing some adapters for different frameworks among them yew.

I have implemented a basic structure for buttons something like this to use like this.

This works fine on web_sys but I'm trying to write the yew adapter and I'm having problems by casting between yew::Callback<web_sys::MouseEvent> to FnMut(web_sys::MouseEvent).

My adapter looks like this...

this is the definition for button in my core module

And this is the definition in my adapter crate:

In adapter props takes both InnerData and ComponentOpts in one structure

// the properties in yew adapter
#[derive(PartialEq, Properties)]
pub struct ElevatedButtonProps {
    pub label: AttrValue,

    #[prop_or(false)]
    pub disable: bool,

    #[prop_or_default]
    pub icon: Option<AttrValue>,

    #[prop_or_default]
    pub onclick: Option<Callback<MouseEvent>>,
}

And the functional component in yew, which is the adapter per se, takes the properties passed to it and with them creates the button with my "core" library and returns it as a virtual node of yew.

/// this is my core library https://gitlab.com/material-rs/material_you_rs/-/tree/agnostic-api/crates/core?ref_type=heads
use core::components::buttons::elevated::{self, ElevatedButtonData, ElevatedButtonOpts};
use web_sys::{HtmlElement, MouseEvent};
use yew::{function_component, virtual_dom::VNode, AttrValue, Callback, Html, Properties};


#[function_component(ElevatedButton)]
pub fn elevated_button(props: &ElevatedButtonProps) -> Html {
    let ElevatedButtonProps {
        label,
        disable,
        icon,
        onclick,
    } = props;

    let icon = if let Some(icon) = icon {
        Some(icon.to_string())
    } else {
        None
    };

    let inner = if let Some(onclick) = onclick {
        // problem is here when i'm trying to cast
        let cb = move |e: MouseEvent| {
            onclick.clone().emit(e.clone());
        };

        ElevatedButtonData {
            label: label.to_string(),
            icon,
            onclick: Some(Box::new(cb.clone())),
        }
    } else {
        ElevatedButtonData {
            label: label.to_string(),
            icon,
            onclick: None,
        }
    };

    let opts = ElevatedButtonOpts { disable: *disable };

    let button: HtmlElement = elevated::ElevatedButton::new(inner, opts);

    VNode::VRef(button.into())
}

But this is not working...

The compiler tells me that the yew::Callback must outlive 'static, but at the same time the macro annotation function_component does not allow me lifetimes annotations.

20 | pub fn elevated_button(props: &ElevatedButtonProps) -> Html {
   |                               - let's call the lifetime of this reference `'1`
...
42 |             onclick: Some(Box::new(cb.clone())),
   |                           ^^^^^^^^^^^^^^^^^^^^ cast requires that `'1` must outlive `'static`

does anyone have any idea how I can deal with this?

edit:

"core" is my library where I'm implementing the components only over web_sys and in the adapter I'm "instantiating" it in a comfortable way depending on the framework of the shift


Solution

  • As your example is too complex and incomplete to be reproduced, I will assume that the following is your actual example:

    pub fn convert_callback(
        onclick: &yew::Callback<web_sys::MouseEvent>,
    ) -> impl FnMut(web_sys::MouseEvent) {
        let cb = move |e: web_sys::MouseEvent| {
            onclick.clone().emit(e.clone());
        };
        cb
    }
    
    error[E0700]: hidden type for `impl FnMut(MouseEvent)` captures lifetime that does not appear in bounds
     --> src\lib.rs:7:5
      |
    2 |     onclick: &yew::Callback<web_sys::MouseEvent>,
      |              ----------------------------------- hidden type `[closure@src\lib.rs:4:14: 4:43]` captures the anonymous lifetime defined here
    3 | ) -> impl FnMut(web_sys::MouseEvent) {
      |      ------------------------------- opaque type defined here
    ...
    7 |     cb
      |     ^^
      |
    help: to declare that `impl FnMut(MouseEvent)` captures `'_`, you can add an explicit `'_` lifetime bound
      |
    3 | ) -> impl FnMut(web_sys::MouseEvent) + '_ {
      |                                      ++++
    

    The reason this fails is because you clone() the onclick object inside of the closure, moving it first before you clone it.

    Also, as a minor remark, cloning e is pretty pointless as you already own it. It just creates a copy, then discards the original object. Use the original object directly instead.

    Then, it should compile:

    pub fn convert_callback(
        onclick: &yew::Callback<web_sys::MouseEvent>,
    ) -> impl FnMut(web_sys::MouseEvent) {
        let onclick = onclick.clone();
        let cb = move |e: web_sys::MouseEvent| {
            onclick.emit(e);
        };
        cb
    }
    

    You can also put those two actions into a nested scope, if you don't want to leak the cloned onclick object into the outer scope:

    pub fn convert_callback(
        onclick: &yew::Callback<web_sys::MouseEvent>,
    ) -> impl FnMut(web_sys::MouseEvent) {
        let cb = {
            let onclick = onclick.clone();
            move |e: web_sys::MouseEvent| {
                onclick.emit(e);
            }
        };
        cb
    }