Running egui applications on the web has been possible for some time. The official template uses trunk to manage all that. The app will render into a fullscreen canvas, taking over the whole page.
It works fine, I've built a few small applications using that pattern. But, it wasn't clear to me how I would integrate a smaller egui app into a larger (html) page. Besides, trunk uses a template html file, and it feels like a little bit too much magic to me.
Now we take the egui, and put it into the component
So, instead, let's properly seperate concerns. We'll compile the egui app into a web component, after which, rust is done. It no longer cares about how it is embedded. We can then write an extremely simple html page and load that web component.
I believe this is easier done than said, so please, go here: https://0x53a.github.io/web-component-rs/.
(The source code is available here: https://github.com/0x53a/web-component-rs).
Because these are just web components, it's trivial to embed third party components:
Expand Me!
Web Components are just html elements, you can load a wasm blob once and instantiate the component multiple times.
Please press F12 to look at the source code.
All that's needed is:
<egui-plotter style="display: block; height: 200px;" />
<script type="module">
import init from 'https://0x53a.github.io/web-component-rs/fourth_component/pkg/fourth_component.js';
await init();
</script>
Now, let's move on to how it internally works.
First, a refresher on how custom web components work:
You only need to define a JS class that derives from
In rust, we define a struct which implements a trait and use the WebComponent derive macro to generate the boilerplate.
#[derive(WebComponent)]
#[web_component(name = "my-counter", observed_attributes = ["value"])]
pub struct MyCounter {
// ...
element: Option<web_sys::HtmlElement>,
}
impl WebComponent for MyCounter {
fn attach(&mut self, element: &web_sys::HtmlElement) {
self.element = Some(element.clone());
}
fn connected(&mut self) {
// ...
}
}
The derive macro creates a function, which will dynamically eval a JS class declaration and register the element.
pub fn setup_mycounter() {
// ...
let class_definition = format!(
r#"
class {js_class} extends HTMLElement {{
constructor() {{
super();
window.__wasm_attach_{js_class}(this);
}}
connectedCallback() {{
window.__wasm_connected_{js_class}(this);
}}
disconnectedCallback() {{
window.__wasm_disconnected_{js_class}(this);
}}
adoptedCallback() {{
window.__wasm_adopted_{js_class}(this);
}}
attributeChangedCallback(name, oldValue, newValue) {{
window.__wasm_attribute_changed_{js_class}(this, name, oldValue, newValue);
}}
static get observedAttributes() {{
return {observed_attrs};
}}
}}
customElements.define('{tag_name}', {js_class});
"#, ...
);
eval(class_definition);
}
The above code is generated, we just need to make sure to call it
#[wasm_bindgen(start)]
pub fn start() {
simple::setup_mycounter();
}
So, with that, all we then need to do is initialise eframe inside the connected callback.
impl WebComponent for MyCounter {
fn connected(&mut self) {
let element = self.element.as_ref().unwrap();
let shadow = element
.attach_shadow(&web_sys::ShadowRootInit::new(web_sys::ShadowRootMode::Open))
.expect("failed to attach shadow root");
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document
.create_element("canvas")
.expect("failed to create canvas")
.unchecked_into::<HtmlCanvasElement>();
let canvas_style = canvas.style();
canvas_style.set_property("display", "block").unwrap();
canvas_style.set_property("width", "100%").unwrap();
canvas_style.set_property("height", "100%").unwrap();
shadow.append_child(&canvas).unwrap();
let runner = eframe::WebRunner::new();
let element_copy = element.clone();
wasm_bindgen_futures::spawn_local(async move {
let result = runner
.start(
canvas,
eframe::WebOptions::default(),
Box::new(|cc| Ok(Box::new(EguiApp::new(cc)))),
)
.await;
if let Err(e) = &result {
web_sys::console::error_1(e);
}
if result.is_ok() {
EguiComponent::with_element(&element_copy, |comp| {
comp.runner = Some(runner);
});
}
});
}
}
So yeah, that's it. I've change one of my applications to the new pattern, you can see the commit here: https://github.com/0x53A/UAS-SigVer/commit/833a8052f24cff59bb90f14e59077fbf21c90c4f
That commit moves some things around so it looks bigger than it is. As an example, with trunk, your rust wasm project is an application with a main function. With web components and wasm-pack, it's a library.