1pub use wasm_bindgen;
2use std::any::{Any, TypeId};
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::rc::Rc;
6
7#[cfg(target_arch = "wasm32")]
11pub use web_sys;
12#[cfg(target_arch = "wasm32")]
13use wasm_bindgen::prelude::*;
14#[cfg(target_arch = "wasm32")]
15use wasm_bindgen::JsCast;
16#[cfg(target_arch = "wasm32")]
17use web_sys::{window, Document, Element, Event, Node, HtmlElement};
18#[cfg(target_arch = "wasm32")]
19use std::cell::Cell;
20#[cfg(target_arch = "wasm32")]
21use serde::de::DeserializeOwned;
22
23#[cfg(not(target_arch = "wasm32"))]
25#[derive(Clone)]
26pub struct Event;
27
28#[cfg(not(target_arch = "wasm32"))]
30pub struct JsValue;
31
32#[cfg(not(target_arch = "wasm32"))]
33pub struct Closure<T: ?Sized> { _marker: std::marker::PhantomData<T> }
34
35#[cfg(not(target_arch = "wasm32"))]
36impl<T: ?Sized> Closure<T> {
37 pub fn wrap(_: Box<T>) -> Self { Self { _marker: std::marker::PhantomData } }
38 pub fn as_ref(&self) -> &JsValue { unimplemented!() }
39 pub fn forget(self) {}
40}
41
42
43#[cfg(target_arch = "wasm32")]
47pub fn init_error_boundary() {
48 std::panic::set_hook(Box::new(|panic_info| {
49 let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() { *s }
50 else if let Some(s) = panic_info.payload().downcast_ref::<String>() { s.as_str() }
51 else { "Unknown Rust Panic inside WebAssembly" };
52 let location = panic_info.location().map(|l| format!("{}:{}", l.file(), l.line())).unwrap_or_else(|| String::from("Unknown location"));
53 let doc = window().unwrap().document().unwrap();
54 let body = doc.body().unwrap();
55 let overlay = doc.create_element("div").unwrap();
56 overlay.set_attribute("style", "position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(30,0,0,0.95);color:#ff5555;padding:3rem;z-index:99999;font-family:monospace;").unwrap();
57 overlay.set_inner_html(&format!(r#"<h1>⚠️ Oxirast Crash Report</h1><div style="background:#220000;border:1px solid #ff0000;padding:1.5rem;"><h3>Error:</h3><p>{}</p><hr/><h3>Location:</h3><p>{}</p></div>"#, msg, location));
58 body.append_child(&overlay).unwrap();
59 web_sys::console::error_1(&format!("Oxirast Panic: {} at {}", msg, location).into());
60 }));
61}
62
63pub type EventCallback = Rc<RefCell<Box<dyn FnMut(Event)>>>;
67pub type LifecycleCallback = Rc<RefCell<Box<dyn FnMut()>>>;
68
69#[derive(Clone)]
70pub enum VNode { Element(VElement), Text(String) }
71
72#[derive(Clone)]
73pub struct VElement {
74 pub tag: String,
75 pub attrs: HashMap<String, String>,
76 pub bound_attrs: HashMap<String, Signal<String>>,
77 pub children: Vec<VNode>,
78 pub events: HashMap<String, EventCallback>,
79 pub bound_text: Option<Signal<String>>,
80 pub bound_show: Option<Signal<bool>>,
81 pub on_mount: Option<LifecycleCallback>,
82 pub on_cleanup: Option<LifecycleCallback>,
83}
84
85impl VNode {
86 pub fn element(tag: &str) -> VElement {
87 VElement { tag: tag.to_string(), attrs: HashMap::new(), bound_attrs: HashMap::new(), children: Vec::new(), events: HashMap::new(), bound_text: None, bound_show: None, on_mount: None, on_cleanup: None }
88 }
89 pub fn text(s: &str) -> VNode { VNode::Text(s.to_string()) }
90}
91
92impl VElement {
93 pub fn attr(mut self, key: &str, val: &str) -> Self { self.attrs.insert(key.to_string(), val.to_string()); self }
94 pub fn bind_attr(mut self, key: &str, sig: Signal<String>) -> Self { self.bound_attrs.insert(key.to_string(), sig); self }
95 pub fn on(mut self, event: &str, cb: EventCallback) -> Self { self.events.insert(event.to_string(), cb); self }
96 pub fn child(mut self, node: VNode) -> Self { self.children.push(node); self }
97 pub fn bind_text(mut self, sig: Signal<String>) -> Self { self.bound_text = Some(sig); self }
98 pub fn bind_show(mut self, sig: Signal<bool>) -> Self { self.bound_show = Some(sig); self }
99 pub fn on_mount(mut self, cb: LifecycleCallback) -> Self { self.on_mount = Some(cb); self }
100 pub fn on_cleanup(mut self, cb: LifecycleCallback) -> Self { self.on_cleanup = Some(cb); self }
101 pub fn build(self) -> VNode { VNode::Element(self) }
102}
103
104pub fn render_to_string(vnode: &VNode) -> String {
108 match vnode {
109 VNode::Text(text) => text.clone(),
110 VNode::Element(el) => {
111 let mut attrs = String::new();
112 for (k, v) in &el.attrs { attrs.push_str(&format!(" {}=\"{}\"", k, v)); }
113 for (k, sig) in &el.bound_attrs { attrs.push_str(&format!(" {}=\"{}\"", k, sig.get())); }
114 if let Some(sig) = &el.bound_show { if !sig.get() { attrs.push_str(" style=\"display:none;\""); } }
115
116 let mut children = String::new();
117 if let Some(sig) = &el.bound_text { children.push_str(&sig.get()); }
118 else { for child in &el.children { children.push_str(&render_to_string(child)); } }
119 format!("<{}{}>{}</{}>", el.tag, attrs, children, el.tag)
120 }
121 }
122}
123
124#[cfg(target_arch = "wasm32")]
128thread_local! {
129 static NEXT_ID: Cell<usize> = Cell::new(1);
130 static EVENT_REGISTRY: RefCell<HashMap<String, Vec<Closure<dyn FnMut(Event)>>>> = RefCell::new(HashMap::new());
131 static CLEANUP_REGISTRY: RefCell<HashMap<String, LifecycleCallback>> = RefCell::new(HashMap::new());
132}
133
134#[cfg(target_arch = "wasm32")]
135pub fn document() -> Document { window().expect("No window").document().expect("No document") }
136#[cfg(target_arch = "wasm32")]
137pub fn mount_to_body(element: &Node) { document().body().unwrap().append_child(element).unwrap(); }
138
139#[cfg(target_arch = "wasm32")]
140pub fn render_vnode(vnode: &VNode) -> Node {
141 let doc = document();
142 match vnode {
143 VNode::Text(text) => doc.create_text_node(text).into(),
144 VNode::Element(vel) => {
145 let el = doc.create_element(&vel.tag).unwrap();
146 for (k, v) in &vel.attrs { el.set_attribute(k, v).ok(); }
147 for (k, sig) in &vel.bound_attrs { el.set_attribute(k, &sig.get()).ok(); let el_c = el.clone(); let k_c = k.clone(); sig.subscribe(move |v| { el_c.set_attribute(&k_c, v).ok(); }); }
148 if let Some(sig) = &vel.bound_text { el.set_text_content(Some(&sig.get())); let el_c = el.clone(); sig.subscribe(move |v| { el_c.set_text_content(Some(v)); }); }
149 if let Some(sig) = &vel.bound_show {
150 let html_el = el.clone().dyn_into::<HtmlElement>().unwrap(); html_el.style().set_property("display", if sig.get() { "" } else { "none" }).unwrap();
151 let el_c = html_el.clone(); sig.subscribe(move |v| { el_c.style().set_property("display", if *v { "" } else { "none" }).unwrap(); });
152 }
153
154 let needs_id = !vel.events.is_empty() || vel.on_cleanup.is_some();
155 let mut id = String::new();
156 if needs_id { id = NEXT_ID.with(|n| { let val = n.get(); n.set(val + 1); val.to_string() }); el.set_attribute("data-ox-id", &id).unwrap(); }
157
158 if !vel.events.is_empty() {
159 let mut closures = Vec::new();
160 for (name, cb) in &vel.events {
161 let cb_c = cb.clone(); let closure = Closure::wrap(Box::new(move |e: Event| { if let Ok(mut f) = cb_c.try_borrow_mut() { f(e); } }) as Box<dyn FnMut(_)>);
162 el.add_event_listener_with_callback(name, closure.as_ref().unchecked_ref()).unwrap(); closures.push(closure);
163 }
164 EVENT_REGISTRY.with(|reg| reg.borrow_mut().insert(id.clone(), closures));
165 }
166 if let Some(m_cb) = &vel.on_mount { if let Ok(mut f) = m_cb.try_borrow_mut() { f(); } }
167 if let Some(c_cb) = &vel.on_cleanup { CLEANUP_REGISTRY.with(|reg| reg.borrow_mut().insert(id.clone(), c_cb.clone())); }
168 for child in &vel.children { el.append_child(&render_vnode(child)).ok(); }
169 el.into()
170 }
171 }
172}
173
174#[cfg(target_arch = "wasm32")]
175pub fn hydrate_dom(dom_node: &Node, vnode: &VNode) {
176 match vnode {
177 VNode::Element(vel) => {
178 if let Ok(el) = dom_node.clone().dyn_into::<Element>() {
179 let needs_id = !vel.events.is_empty() || vel.on_cleanup.is_some();
180 let mut id = String::new();
181 if needs_id { id = NEXT_ID.with(|n| { let val = n.get(); n.set(val + 1); val.to_string() }); let _ = el.set_attribute("data-ox-id", &id); }
182
183 for (k, sig) in &vel.bound_attrs { let el_c = el.clone(); let k_c = k.clone(); sig.subscribe(move |v| { el_c.set_attribute(&k_c, v).ok(); }); }
184 if let Some(sig) = &vel.bound_text { let el_c = el.clone(); sig.subscribe(move |v| { el_c.set_text_content(Some(v)); }); }
185 if let Some(sig) = &vel.bound_show { if let Ok(html_el) = el.clone().dyn_into::<HtmlElement>() { let el_c = html_el.clone(); sig.subscribe(move |v| { el_c.style().set_property("display", if *v { "" } else { "none" }).unwrap(); }); } }
186
187 if !vel.events.is_empty() {
188 let mut closures = Vec::new();
189 for (name, cb) in &vel.events {
190 let cb_c = cb.clone(); let closure = Closure::wrap(Box::new(move |e: Event| { if let Ok(mut f) = cb_c.try_borrow_mut() { f(e); } }) as Box<dyn FnMut(_)>);
191 el.add_event_listener_with_callback(name, closure.as_ref().unchecked_ref()).unwrap(); closures.push(closure);
192 }
193 EVENT_REGISTRY.with(|reg| reg.borrow_mut().insert(id.clone(), closures));
194 }
195 if let Some(m_cb) = &vel.on_mount { if let Ok(mut f) = m_cb.try_borrow_mut() { f(); } }
196 if let Some(c_cb) = &vel.on_cleanup { CLEANUP_REGISTRY.with(|reg| reg.borrow_mut().insert(id.clone(), c_cb.clone())); }
197
198 let child_nodes = el.child_nodes();
199 for i in 0..vel.children.len() { if let Some(child_dom) = child_nodes.item(i as u32) { hydrate_dom(&child_dom, &vel.children[i]); } }
200 }
201 }
202 VNode::Text(_) => {}
203 }
204}
205
206#[cfg(target_arch = "wasm32")]
207pub fn cleanup_node(node: &Node) {
208 if let Ok(el) = node.clone().dyn_into::<Element>() {
209 if let Some(id) = el.get_attribute("data-ox-id") {
210 EVENT_REGISTRY.with(|reg| { reg.borrow_mut().remove(&id); });
211 CLEANUP_REGISTRY.with(|reg| { if let Some(c_cb) = reg.borrow_mut().remove(&id) { if let Ok(mut f) = c_cb.try_borrow_mut() { f(); } } });
212 }
213 let children = el.child_nodes();
214 for i in 0..children.length() { if let Some(child) = children.item(i) { cleanup_node(&child); } }
215 }
216}
217
218#[derive(Clone)]
222pub struct Signal<T> {
223 value: Rc<RefCell<T>>,
224 listeners: Rc<RefCell<Vec<Box<dyn FnMut(&T)>>>>,
225}
226
227impl<T: Clone + 'static> Signal<T> {
228 pub fn new(initial_value: T) -> Self { Self { value: Rc::new(RefCell::new(initial_value)), listeners: Rc::new(RefCell::new(Vec::new())) } }
229 pub fn get(&self) -> T { self.value.borrow().clone() }
230 pub fn set(&self, new_value: T) { *self.value.borrow_mut() = new_value.clone(); for listener in self.listeners.borrow_mut().iter_mut() { listener(&new_value); } }
231 pub fn subscribe<F>(&self, callback: F) where F: FnMut(&T) + 'static { self.listeners.borrow_mut().push(Box::new(callback)); }
232}
233pub fn use_state<T: Clone + 'static>(initial: T) -> Signal<T> { Signal::new(initial) }
234pub fn use_memo<T: Clone + 'static, U: Clone + 'static, F: Fn(&T) -> U + 'static>(dep: &Signal<T>, calc: F) -> Signal<U> {
235 let memo_sig = use_state(calc(&dep.get())); let memo_clone = memo_sig.clone(); dep.subscribe(move |v| { memo_clone.set(calc(v)); }); memo_sig
236}
237pub fn use_effect<T: Clone + 'static, F: FnMut(&T) + 'static>(dep: &Signal<T>, mut effect: F) { effect(&dep.get()); dep.subscribe(effect); }
238
239thread_local! { static GLOBAL_CONTEXT: RefCell<HashMap<TypeId, Rc<dyn Any>>> = RefCell::new(HashMap::new()); }
240pub fn provide_context<T: 'static>(value: T) { GLOBAL_CONTEXT.with(|ctx| { ctx.borrow_mut().insert(TypeId::of::<T>(), Rc::new(value)); }); }
241pub fn use_context<T: Clone + 'static>() -> Option<T> { GLOBAL_CONTEXT.with(|ctx| { ctx.borrow().get(&TypeId::of::<T>()).and_then(|rc| rc.downcast_ref::<T>().cloned()) }) }
242
243
244thread_local! { static ROUTE_PARAMS: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new()); }
248pub fn use_params() -> HashMap<String, String> { ROUTE_PARAMS.with(|p| p.borrow().clone()) }
249
250#[cfg(target_arch = "wasm32")]
251pub fn use_query() -> HashMap<String, String> {
252 let search = window().unwrap().location().search().unwrap_or_default();
253 let mut query = HashMap::new();
254 if search.starts_with('?') {
255 for pair in search[1..].split('&') {
256 if pair.is_empty() { continue; }
257 let mut parts = pair.splitn(2, '=');
258 query.insert(parts.next().unwrap_or_default().to_string(), parts.next().unwrap_or_default().to_string());
259 }
260 }
261 query
262}
263
264#[cfg(target_arch = "wasm32")]
265pub fn current_path() -> String { window().expect("no window").location().pathname().unwrap_or_else(|_| String::from("/")) }
266#[cfg(target_arch = "wasm32")]
267pub fn push_route(path: &str) { window().unwrap().history().unwrap().push_state_with_url(&JsValue::NULL, "", Some(path)).unwrap(); }
268
269#[derive(Clone)]
270pub struct RouteInfo {
271 pub component: fn(Signal<String>) -> VNode,
272 pub guard: Option<(fn() -> bool, String)>,
273}
274
275pub struct Router {
276 root_id: String,
277 routes: HashMap<String, RouteInfo>,
278}
279
280impl Router {
281 pub fn new(root_id: &str) -> Self { Self { root_id: root_id.to_string(), routes: HashMap::new() } }
282
283 pub fn route(mut self, path: &str, component: fn(Signal<String>) -> VNode) -> Self {
284 self.routes.insert(path.to_string(), RouteInfo { component, guard: None }); self
285 }
286 pub fn guarded_route(mut self, path: &str, component: fn(Signal<String>) -> VNode, guard: fn() -> bool, redirect: &str) -> Self {
287 self.routes.insert(path.to_string(), RouteInfo { component, guard: Some((guard, redirect.to_string())) }); self
288 }
289
290 pub fn render_route_to_string(&self, request_path: &str) -> String {
293 let dummy_nav = use_state(request_path.to_string());
294 for (route_path, info) in self.routes.iter() {
295 if route_path == request_path {
296 let vnode = (info.component)(dummy_nav);
297 return render_to_string(&vnode);
298 }
299 }
300 String::from("<h1>404 Not Found</h1>")
301 }
302
303 #[cfg(target_arch = "wasm32")]
305 pub fn start(self) {
306 init_error_boundary();
307 let route_signal = use_state(current_path());
308 let routes = self.routes.clone();
309 let root_id = self.root_id.clone();
310 let nav_signal = route_signal.clone();
311 let is_initial_load = Rc::new(std::cell::Cell::new(true));
312
313 route_signal.subscribe(move |new_path| {
314 if current_path() != *new_path { push_route(new_path); }
315 let root = document().get_element_by_id(&root_id).expect("Root div not found!");
316
317 let mut matched_route = None;
318 let mut extracted_params = HashMap::new();
319
320 for (route_path, info) in routes.iter() {
321 let route_segments: Vec<&str> = route_path.split('/').collect();
322 let url_segments: Vec<&str> = new_path.split('/').collect();
323 let mut is_match = true;
324 let mut local_params = HashMap::new();
325
326 for (i, rs) in route_segments.iter().enumerate() {
327 if *rs == "*" { local_params.insert("wildcard".to_string(), url_segments[i..].join("/")); break; }
328 if i >= url_segments.len() { is_match = false; break; }
329 if rs.starts_with(':') { local_params.insert(rs[1..].to_string(), url_segments[i].to_string()); }
330 else if *rs != url_segments[i] { is_match = false; break; }
331 }
332 if is_match && (route_segments.last() == Some(&"*") || route_segments.len() == url_segments.len()) {
333 matched_route = Some(info); extracted_params = local_params; break;
334 }
335 }
336
337 ROUTE_PARAMS.with(|p| { *p.borrow_mut() = extracted_params; });
338
339 let page_vnode = match matched_route {
340 Some(info) => {
341 if let Some((guard_fn, redirect)) = &info.guard { if !guard_fn() { nav_signal.set(redirect.clone()); return; } }
342 (info.component)(nav_signal.clone())
343 },
344 None => VNode::element("h1").child(VNode::text("404 Not Found")).build()
345 };
346
347 if is_initial_load.get() {
348 is_initial_load.set(false);
349 if root.child_nodes().length() > 0 { if let Some(first) = root.child_nodes().item(0) { hydrate_dom(&first, &page_vnode); } }
350 else { cleanup_node(&root); root.set_inner_html(""); root.append_child(&render_vnode(&page_vnode)).unwrap(); }
351 } else { cleanup_node(&root); root.set_inner_html(""); root.append_child(&render_vnode(&page_vnode)).unwrap(); }
352 });
353
354 let nav_signal_for_pop = route_signal.clone();
355 let closure = Closure::wrap(Box::new(move |_e: JsValue| { nav_signal_for_pop.set(current_path()); }) as Box<dyn FnMut(JsValue)>);
356 window().unwrap().add_event_listener_with_callback("popstate", closure.as_ref().unchecked_ref()).unwrap();
357 closure.forget();
358 route_signal.set(current_path());
359 }
360}
361
362#[cfg(target_arch = "wasm32")]
366pub fn use_fetch<T>(url: &str) -> (Signal<Option<T>>, Signal<bool>, Signal<Option<String>>)
367where T: DeserializeOwned + Clone + 'static, {
368 let data: Signal<Option<T>> = use_state(None);
369 let is_loading = use_state(true);
370 let error: Signal<Option<String>> = use_state(None);
371
372 let data_c = data.clone(); let loading_c = is_loading.clone(); let error_c = error.clone(); let url_s = url.to_string();
373
374 wasm_bindgen_futures::spawn_local(async move {
375 match reqwest::get(&url_s).await {
376 Ok(resp) => match resp.json::<T>().await { Ok(json) => data_c.set(Some(json)), Err(_) => error_c.set(Some("Parse Error".to_string())), },
377 Err(e) => error_c.set(Some(e.to_string())),
378 }
379 loading_c.set(false);
380 });
381 (data, is_loading, error)
382}
383
384
385#[cfg(target_arch = "wasm32")]
389#[wasm_bindgen(inline_js = "
390 export async function connect_evm() {
391 if (window.ethereum) {
392 const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
393 return accounts[0];
394 }
395 throw new Error('MetaMask not found');
396 }
397 export async function connect_sol() {
398 if (window.solana && window.solana.isPhantom) {
399 const resp = await window.solana.connect();
400 return resp.publicKey.toString();
401 }
402 throw new Error('Phantom wallet not found');
403 }
404")]
405extern "C" {
406 #[wasm_bindgen(catch)]
407 async fn connect_evm() -> Result<JsValue, JsValue>;
408 #[wasm_bindgen(catch)]
409 async fn connect_sol() -> Result<JsValue, JsValue>;
410}
411
412#[cfg(target_arch = "wasm32")]
413pub fn use_wallet(chain: &str) -> (Signal<Option<String>>, Signal<bool>, Rc<dyn Fn()>) {
414 let address: Signal<Option<String>> = use_state(None);
415 let is_connecting = use_state(false);
416
417 let addr_clone = address.clone();
418 let loading_clone = is_connecting.clone();
419 let chain_type = chain.to_string();
420
421 let connect_fn = Rc::new(move || {
422 let addr_c = addr_clone.clone();
423 let load_c = loading_clone.clone();
424 let ct = chain_type.clone();
425
426 load_c.set(true);
427
428 wasm_bindgen_futures::spawn_local(async move {
429 let result = if ct == "solana" { connect_sol().await } else { connect_evm().await };
430 match result {
431 Ok(val) => if let Some(s) = val.as_string() { addr_c.set(Some(s)); },
432 Err(e) => web_sys::console::error_1(&e),
433 }
434 load_c.set(false);
435 });
436 });
437
438 (address, is_connecting, connect_fn)
439}