1use crate::protocol::{HostEvent, ProtocolError, decode_event};
2use crate::renderer::ComposeRenderer;
3use crate::schema::{
4 AssetKind, EventPayload, IconRole, LoopMode, PROTOCOL_VERSION, SCHEMA_HASH, Theme,
5};
6use crate::{Element, KeyEvent, RangeRequest, Selection, VirtualDom};
7use dioxus_core::{ElementId, Event};
8use std::cell::RefCell;
9use std::ffi::c_int;
10use std::future::Future;
11use std::panic::{AssertUnwindSafe, catch_unwind};
12use std::pin::pin;
13use std::rc::Rc;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::{Arc, Mutex, OnceLock};
16use std::task::{Context, Poll, Wake, Waker};
17
18pub const STATUS_OK: i32 = 0;
19pub const STATUS_PROTOCOL_ERROR: i32 = -1;
20pub const STATUS_NOT_INITIALIZED: i32 = -2;
21pub const STATUS_ALREADY_INITIALIZED: i32 = -3;
22pub const STATUS_PANIC: i32 = -4;
23pub const STATUS_NO_RENDERER: i32 = -5;
26
27const EXIT_FAILURE: i32 = 1;
32
33#[repr(C)]
34#[derive(Clone, Copy, Debug)]
35pub struct MutationBatch {
36 pub ptr: *const u8,
37 pub len: u32,
38 pub result: i64,
39}
40
41impl Default for MutationBatch {
42 fn default() -> Self {
43 Self {
44 ptr: std::ptr::null(),
45 len: 0,
46 result: 0,
47 }
48 }
49}
50
51#[derive(Clone, Copy)]
52pub struct RendererApi {
53 pub run: extern "C" fn() -> c_int,
54 pub request_frame: extern "C" fn(),
55}
56
57static RENDERER_API: OnceLock<RendererApi> = OnceLock::new();
58static FRAME_REQUESTED: AtomicBool = AtomicBool::new(false);
59static EVENT_DISPATCH_ACTIVE: AtomicBool = AtomicBool::new(false);
60static DEFERRED_FRAME_REQUEST: AtomicBool = AtomicBool::new(false);
61static LIFECYCLE_SUPPRESSED: AtomicBool = AtomicBool::new(false);
65
66pub fn install_renderer_api(api: RendererApi) -> Result<(), RendererApi> {
67 RENDERER_API.set(api)
68}
69
70#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
74unsafe extern "C" {
75 fn dioxus_compose_renderer_run() -> c_int;
76 fn dioxus_compose_renderer_request_frame();
77}
78
79#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
80extern "C" fn native_run() -> c_int {
81 unsafe { dioxus_compose_renderer_run() }
83}
84
85#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
86extern "C" fn native_request_frame() {
87 unsafe { dioxus_compose_renderer_request_frame() }
89}
90
91#[cfg(any(test, feature = "mock-renderer"))]
94extern "C" fn native_run() -> c_int {
95 STATUS_OK
96}
97
98#[cfg(any(test, feature = "mock-renderer"))]
99extern "C" fn native_request_frame() {}
100
101#[cfg(all(not(renderer_linked), not(any(test, feature = "mock-renderer"))))]
105extern "C" fn native_run() -> c_int {
106 eprintln!("{}", no_renderer_message());
107 STATUS_NO_RENDERER
108}
109
110#[cfg(all(not(renderer_linked), not(any(test, feature = "mock-renderer"))))]
111extern "C" fn native_request_frame() {}
112
113pub fn no_renderer_message() -> String {
118 format!(
119 "dioxus-compose: this application was built without a renderer, so there is nothing\n\
120 to draw with and nothing to draw on. Exiting {EXIT_FAILURE} rather than looking like\n\
121 a program that ran and finished.\n\
122 \n\
123 A default `cargo build` links the renderer for the platform it is building for and\n\
124 downloads it if it has to. A build reaches this message by turning that off:\n\
125 \n\
126 \x20 default-features = false, without re-enabling `native-renderer`\n\
127 \x20 a documentation build, which has no network to fetch a renderer with\n\
128 \n\
129 Building for a test or with the `mock-renderer` feature is a third way, and that\n\
130 one is silent on purpose: those builds drive the boundary directly and want no\n\
131 window."
132 )
133}
134
135fn renderer_api() -> RendererApi {
136 RENDERER_API.get().copied().unwrap_or(RendererApi {
137 run: native_run,
138 request_frame: native_request_frame,
139 })
140}
141
142pub fn request_frame_from_worker() {
151 if !FRAME_REQUESTED.swap(true, Ordering::AcqRel) {
152 if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire)
153 || EVENT_DISPATCH_ACTIVE.load(Ordering::Acquire)
154 {
155 DEFERRED_FRAME_REQUEST.store(true, Ordering::Release);
156 } else {
157 (renderer_api().request_frame)();
158 FRAME_REQUESTED.store(false, Ordering::Release);
159 }
160 }
161}
162
163struct EventDispatchGuard;
164
165impl EventDispatchGuard {
166 fn enter() -> Self {
167 EVENT_DISPATCH_ACTIVE.store(true, Ordering::Release);
168 Self
169 }
170}
171
172impl Drop for EventDispatchGuard {
173 fn drop(&mut self) {
174 EVENT_DISPATCH_ACTIVE.store(false, Ordering::Release);
175 if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire) {
176 return;
177 }
178 if DEFERRED_FRAME_REQUEST.swap(false, Ordering::AcqRel) {
179 (renderer_api().request_frame)();
180 FRAME_REQUESTED.store(false, Ordering::Release);
181 }
182 }
183}
184
185struct FrameWake;
186
187impl Wake for FrameWake {
188 fn wake(self: Arc<Self>) {
189 request_frame_from_worker();
190 }
191
192 fn wake_by_ref(self: &Arc<Self>) {
193 request_frame_from_worker();
194 }
195}
196
197struct PendingAppend {
199 node_id: u32,
200 text: String,
201 dirty: bool,
202}
203
204pub struct Host {
205 app: fn() -> Element,
208 theme: Theme,
209 window: crate::schema::Window,
210 dom: VirtualDom,
211 renderer: ComposeRenderer,
212 frame_waker: Waker,
213 pending_appends: Vec<PendingAppend>,
214}
215
216impl Host {
217 pub fn new(app: fn() -> Element) -> Self {
218 Self::with_theme(app, launched_theme())
219 }
220
221 pub fn with_theme(app: fn() -> Element, theme: Theme) -> Self {
224 crate::window::reset_window_size();
229 crate::message::reset_messages();
233 crate::asset::requeue_all();
242 Self {
243 app,
244 theme,
245 window: launched_window(),
246 dom: VirtualDom::new(app),
247 renderer: ComposeRenderer::new(),
248 frame_waker: Waker::from(Arc::new(FrameWake)),
249 pending_appends: Vec::new(),
250 }
251 }
252
253 pub fn rebuild(&mut self) -> Result<&[u8], ProtocolError> {
254 self.renderer.begin_frame();
255 self.renderer.set_theme(self.theme);
259 self.renderer.set_window(self.window);
263 self.dom.rebuild(&mut self.renderer);
264 self.flush_messages();
265 self.arm_scheduler_wake();
266 self.renderer.finish_frame()
267 }
268
269 pub fn dispatch_event(&mut self, bytes: &[u8]) -> Result<(&[u8], i64), ProtocolError> {
270 let event = decode_event(bytes)?;
271 self.dispatch(event)
272 }
273
274 pub fn arena(&self) -> (*const u8, usize) {
276 self.renderer.arena()
277 }
278
279 pub fn resync(&mut self) -> Result<(&[u8], i64), ProtocolError> {
286 *self = Self::with_theme(self.app, self.theme);
287 Ok((self.rebuild()?, 0))
288 }
289
290 fn set_lifecycle_running(&mut self, running: bool) -> Result<(&[u8], i64), ProtocolError> {
293 LIFECYCLE_SUPPRESSED.store(!running, Ordering::Release);
294 if running {
295 if DEFERRED_FRAME_REQUEST.swap(false, Ordering::AcqRel) {
296 (renderer_api().request_frame)();
297 }
298 FRAME_REQUESTED.store(false, Ordering::Release);
299 }
300 self.renderer.begin_frame();
303 Ok((self.renderer.finish_frame()?, 0))
304 }
305
306 fn publish_design_system(
312 &mut self,
313 system: crate::schema::DesignSystem,
314 ) -> Result<(&[u8], i64), ProtocolError> {
315 if crate::design::publish(system) {
316 self.renderer.begin_frame();
317 self.dom.render_immediate(&mut self.renderer);
318 self.flush_messages();
319 self.arm_scheduler_wake();
320 return Ok((self.renderer.finish_frame()?, 0));
321 }
322 self.renderer.begin_frame();
323 Ok((self.renderer.finish_frame()?, 0))
324 }
325
326 pub fn dispatch(&mut self, event: HostEvent<'_>) -> Result<(&[u8], i64), ProtocolError> {
327 match event.payload {
330 EventPayload::Resync => return self.resync(),
331 EventPayload::DesignSystemResolved(system) => {
332 return self.publish_design_system(system);
333 }
334 EventPayload::LifecycleStart => return self.set_lifecycle_running(true),
335 EventPayload::LifecycleStop => return self.set_lifecycle_running(false),
336 _ => {}
337 }
338 if let EventPayload::WindowSizeChanged {
342 width_dp,
343 height_dp,
344 class,
345 height_class,
346 } = event.payload
347 {
348 let size = crate::window::WindowSize::new(width_dp, height_dp, class, height_class);
352 if event.node_id != 0 {
353 let woke = self
354 .renderer
355 .size_token(event.node_id)
356 .is_some_and(|token| crate::window::publish_node(token, size));
357 if !woke {
358 self.renderer.begin_frame();
359 return Ok((self.renderer.finish_frame()?, 0));
360 }
361 self.renderer.begin_frame();
362 self.dom.render_immediate(&mut self.renderer);
363 self.flush_messages();
364 self.arm_scheduler_wake();
365 return Ok((self.renderer.finish_frame()?, 0));
366 }
367 crate::window::publish(size);
368 self.renderer.begin_frame();
369 self.dom.render_immediate(&mut self.renderer);
370 self.flush_messages();
371 self.arm_scheduler_wake();
372 return Ok((self.renderer.finish_frame()?, 0));
373 }
374 let message_action = match event.payload {
378 EventPayload::Clicked => crate::message::take_action(event.handler_id),
379 _ => None,
380 };
381 if let Some(action) = message_action {
382 if event.node_id != 0 {
383 return Err(ProtocolError::InvalidValueKind(0));
384 }
385 let _dispatch_guard = EventDispatchGuard::enter();
386 action.call(());
387 self.renderer.begin_frame();
388 self.dom.render_immediate(&mut self.renderer);
389 self.flush_messages();
390 self.arm_scheduler_wake();
391 return Ok((self.renderer.finish_frame()?, 0));
392 }
393 let Some((element, node_id, name)) = self.renderer.handler(event.handler_id) else {
394 return Err(ProtocolError::InvalidValueKind(0));
395 };
396 if event.node_id != node_id {
397 return Err(ProtocolError::InvalidValueKind(0));
398 }
399 let mut key_event = None;
400 let event_data = match event.payload {
401 EventPayload::Clicked | EventPayload::FocusLost => {
402 Event::new(Rc::new(()), true).into_any()
403 }
404 EventPayload::TextChanged(text) | EventPayload::TextSubmitted(text) => {
405 Event::new(Rc::new(text.to_owned()), true).into_any()
406 }
407 EventPayload::ProtocolError { .. } => {
408 return Err(ProtocolError::InvalidValueKind(0));
409 }
410 EventPayload::KeyDown {
411 key,
412 shift_key,
413 ctrl_key,
414 alt_key,
415 meta_key,
416 } => {
417 let value = KeyEvent::new(key, shift_key, ctrl_key, alt_key, meta_key);
418 key_event = Some(value.clone());
419 Event::new(Rc::new(value), true).into_any()
420 }
421 EventPayload::FilesEntered => Event::new(Rc::new(()), true).into_any(),
422 EventPayload::FilesDropped(paths) => {
423 Event::new(Rc::new(crate::widgets::FileDrop::new(paths)), true).into_any()
424 }
425 EventPayload::RangeRequested { start, count } => {
426 Event::new(Rc::new(RangeRequest::new(start, count)), true).into_any()
427 }
428 EventPayload::ValueChanged(value) => Event::new(Rc::new(value), true).into_any(),
429 EventPayload::WindowSizeChanged { .. }
430 | EventPayload::DesignSystemResolved(_)
431 | EventPayload::Resync
432 | EventPayload::LifecycleStart
433 | EventPayload::LifecycleStop => unreachable!("handled above"),
434 };
435 let _dispatch_guard = EventDispatchGuard::enter();
436 self.dom.runtime().handle_event(name, event_data, element);
437 self.renderer.begin_frame();
438 self.dom.render_immediate(&mut self.renderer);
439 self.flush_messages();
440 self.arm_scheduler_wake();
441 let result = i64::from(key_event.is_some_and(|event| event.consumed()));
442 Ok((self.renderer.finish_frame()?, result))
443 }
444
445 pub fn render_frame(&mut self, _frame_time_nanos: u64) -> Result<&[u8], ProtocolError> {
446 FRAME_REQUESTED.store(false, Ordering::Release);
447 EVENT_DISPATCH_ACTIVE.store(false, Ordering::Release);
448 DEFERRED_FRAME_REQUEST.store(false, Ordering::Release);
449 self.renderer.begin_frame();
450 self.dom.render_immediate(&mut self.renderer);
451 self.flush_pending_appends();
452 self.flush_messages();
453 self.arm_scheduler_wake();
454 self.renderer.finish_frame()
455 }
456
457 pub fn append_text(&mut self, node_id: u32, tail: &str) {
460 match self
461 .pending_appends
462 .iter_mut()
463 .find(|pending| pending.node_id == node_id)
464 {
465 Some(pending) => {
466 pending.text.push_str(tail);
467 pending.dirty = true;
468 }
469 None => self.pending_appends.push(PendingAppend {
470 node_id,
471 text: tail.to_owned(),
472 dirty: true,
473 }),
474 }
475 request_frame_from_worker();
477 }
478
479 fn flush_messages(&mut self) {
484 let renderer = &mut self.renderer;
485 crate::asset::drain(|pending| {
489 renderer.register_asset(pending.asset_id, pending.kind, pending.bytes);
490 });
491 crate::message::drain(|message| {
492 renderer.show_message(
493 message.handler_id,
494 &message.text,
495 &message.action,
496 message.duration,
497 );
498 });
499 }
500
501 fn flush_pending_appends(&mut self) {
502 for index in 0..self.pending_appends.len() {
503 let pending = &self.pending_appends[index];
504 if pending.dirty {
505 self.renderer
506 .append_text_node(pending.node_id, &pending.text);
507 }
508 }
509 for pending in &mut self.pending_appends {
512 pending.text.clear();
513 pending.dirty = false;
514 }
515 }
516
517 pub fn register_asset(
523 &mut self,
524 asset_id: u32,
525 kind: AssetKind,
526 bytes: &[u8],
527 ) -> Result<&[u8], ProtocolError> {
528 self.renderer.begin_frame();
529 self.renderer.register_asset(asset_id, kind, bytes);
530 self.renderer.finish_frame()
531 }
532
533 pub fn register_icon(&mut self, asset_id: u32, role: IconRole) -> Result<&[u8], ProtocolError> {
536 self.register_asset(
537 asset_id,
538 AssetKind::VectorIcon,
539 &(role as u16).to_le_bytes(),
540 )
541 }
542
543 pub fn release_asset(&mut self, asset_id: u32) -> Result<&[u8], ProtocolError> {
546 self.renderer.begin_frame();
547 self.renderer.release_asset(asset_id);
548 self.renderer.finish_frame()
549 }
550
551 pub fn set_text(
552 &mut self,
553 node_id: u32,
554 text: &str,
555 selection: Option<Selection>,
556 ) -> Result<&[u8], ProtocolError> {
557 self.renderer.begin_frame();
558 self.renderer.set_text_node(node_id, text, selection);
559 self.renderer.finish_frame()
560 }
561
562 pub fn handler_target(&self, handler_id: u64) -> Option<ElementId> {
563 self.renderer.handler(handler_id).map(|value| value.0)
564 }
565
566 fn arm_scheduler_wake(&mut self) {
567 let mut context = Context::from_waker(&self.frame_waker);
568 let future = self.dom.wait_for_work();
569 let mut future = pin!(future);
570 if future.as_mut().poll(&mut context) == Poll::Ready(()) {
571 request_frame_from_worker();
572 }
573 }
574}
575
576struct HostSlot(RefCell<Option<Host>>);
589
590impl Drop for HostSlot {
591 fn drop(&mut self) {
592 std::mem::forget(self.0.borrow_mut().take());
593 }
594}
595
596impl std::ops::Deref for HostSlot {
597 type Target = RefCell<Option<Host>>;
598
599 fn deref(&self) -> &Self::Target {
600 &self.0
601 }
602}
603
604static APP: Mutex<Option<fn() -> Element>> = Mutex::new(None);
618
619static THEME: Mutex<Theme> = Mutex::new(Theme::unified(crate::schema::DesignSystem::Material3));
621
622static WINDOW: Mutex<crate::schema::Window> = Mutex::new(crate::schema::Window::new());
624
625thread_local! {
626 static HOST: HostSlot = const { HostSlot(RefCell::new(None)) };
627}
628
629fn launched_app() -> Option<fn() -> Element> {
630 APP.lock().map_or(None, |app| *app)
631}
632
633fn launched_window() -> crate::schema::Window {
634 WINDOW
635 .lock()
636 .map_or_else(|error| *error.into_inner(), |window| *window)
637}
638
639fn launched_theme() -> Theme {
640 THEME
641 .lock()
642 .map_or_else(|error| *error.into_inner(), |theme| *theme)
643}
644
645#[derive(Clone, Copy, Debug)]
646pub struct LaunchBuilder {
647 mode: LoopMode,
648 theme: Theme,
649 window: crate::schema::Window,
650}
651
652impl Default for LaunchBuilder {
653 fn default() -> Self {
654 Self {
655 mode: LoopMode::Renderer,
656 theme: Theme::default(),
657 window: crate::schema::Window::new(),
658 }
659 }
660}
661
662impl LaunchBuilder {
663 pub fn new() -> Self {
664 Self::default()
665 }
666
667 pub fn with_mode(mut self, mode: LoopMode) -> Self {
668 self.mode = mode;
669 self
670 }
671
672 pub fn with_theme(mut self, theme: Theme) -> Self {
676 self.theme = theme;
677 self
678 }
679
680 pub fn with_window(mut self, window: crate::schema::Window) -> Self {
688 self.window = window;
689 self
690 }
691
692 pub fn launch(self, app: fn() -> Element) {
700 let status = self.try_launch(app);
701 if status != STATUS_OK {
702 std::process::exit(EXIT_FAILURE);
703 }
704 }
705
706 pub fn try_launch(self, app: fn() -> Element) -> i32 {
709 if let Ok(mut slot) = APP.lock() {
710 *slot = Some(app);
711 }
712 if let Ok(mut slot) = THEME.lock() {
713 *slot = self.theme;
714 }
715 if let Ok(mut slot) = WINDOW.lock() {
716 *slot = self.window;
717 }
718 if self.mode != LoopMode::Renderer {
721 return STATUS_OK;
722 }
723 (renderer_api().run)()
724 }
725}
726
727pub fn launch(app: fn() -> Element) {
728 LaunchBuilder::new().launch(app);
729}
730
731fn parse_handshake(bytes: &[u8]) -> Result<LoopMode, ProtocolError> {
732 if bytes.len() < 12 {
733 return Err(ProtocolError::Truncated);
734 }
735 let hash = u64::from_le_bytes(
736 bytes[0..8]
737 .try_into()
738 .map_err(|_| ProtocolError::Truncated)?,
739 );
740 let version = u16::from_le_bytes(
741 bytes[8..10]
742 .try_into()
743 .map_err(|_| ProtocolError::Truncated)?,
744 );
745 if hash != SCHEMA_HASH || version != PROTOCOL_VERSION {
746 eprintln!(
754 "dioxus-compose: the renderer was built from a different schema than this program. It sent hash {hash:#x} version {version}, and this build expects hash {SCHEMA_HASH:#x} version {PROTOCOL_VERSION}. Rebuild the renderer after running codegen; if it was already rebuilt, its build directory is holding a cached copy of the generated protocol and has to be cleared."
755 );
756 return Err(ProtocolError::InvalidEnvelope);
757 }
758 match bytes[10] {
759 0 => Ok(LoopMode::Renderer),
760 1 => Ok(LoopMode::Platform),
761 other => Err(ProtocolError::InvalidValueKind(u16::from(other))),
762 }
763}
764
765unsafe fn input_slice<'a>(ptr: *const u8, len: u32) -> Result<&'a [u8], ProtocolError> {
766 if len == 0 {
767 return Ok(&[]);
768 }
769 if ptr.is_null() {
770 return Err(ProtocolError::Truncated);
771 }
772 Ok(unsafe { std::slice::from_raw_parts(ptr, len as usize) })
774}
775
776unsafe fn write_batch(
777 out: *mut MutationBatch,
778 bytes: &[u8],
779 result: i64,
780) -> Result<(), ProtocolError> {
781 if out.is_null() {
782 return Err(ProtocolError::Truncated);
783 }
784 let len = u32::try_from(bytes.len()).map_err(|_| ProtocolError::LengthOverflow)?;
785 unsafe {
787 out.write(MutationBatch {
788 ptr: bytes.as_ptr(),
789 len,
790 result,
791 });
792 }
793 Ok(())
794}
795
796fn ffi_status(operation: impl FnOnce() -> Result<(), i32>) -> i32 {
800 match catch_unwind(AssertUnwindSafe(operation)) {
804 Ok(Ok(())) => STATUS_OK,
805 Ok(Err(status)) => status,
806 Err(_) => STATUS_PANIC,
807 }
808}
809
810fn protocol_error(_error: ProtocolError) -> i32 {
812 STATUS_PROTOCOL_ERROR
813}
814
815#[unsafe(no_mangle)]
816pub unsafe extern "C" fn dioxus_compose_host_init(
821 handshake: *const u8,
822 len: u32,
823 out: *mut MutationBatch,
824) -> i32 {
825 ffi_status(|| {
826 let bytes = unsafe { input_slice(handshake, len) }.map_err(protocol_error)?;
828 let _mode = parse_handshake(bytes).map_err(protocol_error)?;
829 let app = launched_app().ok_or(STATUS_NOT_INITIALIZED)?;
830 HOST.with(|slot| {
831 let mut host_slot = slot.borrow_mut();
832 if host_slot.is_some() {
833 return Err(STATUS_ALREADY_INITIALIZED);
834 }
835 let mut host = Host::new(app);
836 let batch = host.rebuild().map_err(protocol_error)?;
837 unsafe { write_batch(out, batch, 0) }.map_err(protocol_error)?;
839 *host_slot = Some(host);
840 Ok(())
841 })
842 })
843}
844
845#[unsafe(no_mangle)]
846pub unsafe extern "C" fn dioxus_compose_host_dispatch_event(
851 event: *const u8,
852 len: u32,
853 out: *mut MutationBatch,
854) -> i32 {
855 ffi_status(|| {
856 let bytes = unsafe { input_slice(event, len) }.map_err(protocol_error)?;
858 HOST.with(|slot| {
859 let mut host_slot = slot.borrow_mut();
860 let host = host_slot.as_mut().ok_or(STATUS_NOT_INITIALIZED)?;
861 let (batch, result) = host.dispatch_event(bytes).map_err(protocol_error)?;
862 unsafe { write_batch(out, batch, result) }.map_err(protocol_error)
864 })
865 })
866}
867
868#[unsafe(no_mangle)]
869pub unsafe extern "C" fn dioxus_compose_host_render_frame(
874 frame_time_nanos: u64,
875 out: *mut MutationBatch,
876) -> i32 {
877 ffi_status(|| {
878 HOST.with(|slot| {
879 let mut host_slot = slot.borrow_mut();
880 let host = host_slot.as_mut().ok_or(STATUS_NOT_INITIALIZED)?;
881 let batch = host
882 .render_frame(frame_time_nanos)
883 .map_err(protocol_error)?;
884 unsafe { write_batch(out, batch, 0) }.map_err(protocol_error)
886 })
887 })
888}
889
890#[unsafe(no_mangle)]
891pub unsafe extern "C" fn dioxus_compose_host_release_batch(batch: *mut MutationBatch) {
896 let _ = catch_unwind(AssertUnwindSafe(|| {
897 if !batch.is_null() {
898 unsafe { batch.write(MutationBatch::default()) };
900 }
901 }));
902}
903
904pub fn current_arena() -> (*const u8, usize) {
909 HOST.with(|slot| {
910 slot.borrow()
911 .as_ref()
912 .map_or((std::ptr::null(), 0), Host::arena)
913 })
914}
915
916#[unsafe(no_mangle)]
917pub extern "C" fn dioxus_compose_host_shutdown() {
918 let _ = catch_unwind(AssertUnwindSafe(|| {
919 HOST.with(|slot| *slot.borrow_mut() = None);
920 FRAME_REQUESTED.store(false, Ordering::Release);
921 }));
922}
923
924#[cfg(test)]
925mod tests {
926 use super::*;
927 use crate::prelude::*;
928 use crate::protocol::{Mutation, PropertyValue, decode_batch};
929 use crate::schema::PropertyKind;
930 use std::collections::HashMap;
931 use std::sync::atomic::{AtomicUsize, Ordering};
932
933 static CLICKS: AtomicUsize = AtomicUsize::new(0);
934
935 static LAUNCH: Mutex<()> = Mutex::new(());
940
941 fn launch_guard() -> std::sync::MutexGuard<'static, ()> {
942 LAUNCH
943 .lock()
944 .unwrap_or_else(|poisoned| poisoned.into_inner())
945 }
946
947 fn app() -> Element {
948 let mut count = use_signal(|| 0_i64);
949 rsx! {
950 Column {
951 Text { text: count().to_string() }
952 TextField { placeholder: "Message" }
953 Button {
954 text: "Increment",
955 on_click: move |_| {
956 CLICKS.fetch_add(1, Ordering::SeqCst);
957 *count.write() += 1;
958 }
959 }
960 }
961 }
962 }
963
964 fn key_app() -> Element {
965 rsx! {
966 Column {
967 TextField {
968 multiline: true,
969 on_key_down: move |event: KeyEvent| {
970 if event.key() == Key::Enter && !event.shift_key() {
971 event.consume();
972 }
973 }
974 }
975 TextField {
976 on_key_down: move |_event: KeyEvent| {}
977 }
978 }
979 }
980 }
981
982 #[derive(Default)]
983 struct MockRenderer {
984 widgets: HashMap<u32, crate::WidgetKind>,
985 parents: HashMap<u32, u32>,
986 }
987
988 impl MockRenderer {
989 fn apply(&mut self, batch: &[Mutation<'_>]) {
990 for mutation in batch {
991 match mutation {
992 Mutation::Create { node_id, widget } => {
993 self.widgets.insert(*node_id, *widget);
994 }
995 Mutation::Insert {
996 parent_id, node_id, ..
997 }
998 | Mutation::Move {
999 parent_id, node_id, ..
1000 } => {
1001 self.parents.insert(*node_id, *parent_id);
1002 }
1003 Mutation::Remove { node_id } => {
1004 self.widgets.remove(node_id);
1005 self.parents.remove(node_id);
1006 }
1007 Mutation::SetProp { .. }
1008 | Mutation::SetModifier { .. }
1009 | Mutation::SetText { .. }
1010 | Mutation::AppendText { .. }
1011 | Mutation::RegisterAsset { .. }
1012 | Mutation::ReleaseAsset { .. }
1013 | Mutation::ShowMessage { .. }
1014 | Mutation::SetTheme(_)
1015 | Mutation::SetWindow(_) => {}
1016 }
1017 }
1018 }
1019 }
1020
1021 #[test]
1022 fn click_runs_once_and_emits_only_text_set_prop() {
1023 CLICKS.store(0, Ordering::SeqCst);
1024 let mut host = Host::new(app);
1025 let initial = decode_batch(host.rebuild().unwrap()).unwrap();
1026 let mut mock = MockRenderer::default();
1027 mock.apply(&initial);
1028 assert_eq!(mock.widgets.len(), 4);
1029 assert_eq!(mock.parents.len(), 3);
1030
1031 let (button_node, handler_id) = initial
1032 .iter()
1033 .find_map(|mutation| match mutation {
1034 Mutation::SetProp {
1035 node_id,
1036 property: PropertyKind::OnClick,
1037 value: PropertyValue::Integer(handler),
1038 } => Some((*node_id, *handler as u64)),
1039 _ => None,
1040 })
1041 .unwrap();
1042 drop(initial);
1043
1044 let event = HostEvent {
1045 node_id: button_node,
1046 handler_id,
1047 payload: EventPayload::Clicked,
1048 };
1049 let (batch, result) = host.dispatch(event).unwrap();
1050 let mutations = decode_batch(batch).unwrap();
1051 assert_eq!(CLICKS.load(Ordering::SeqCst), 1);
1052 assert_eq!(result, 0);
1053 assert_eq!(mutations.len(), 1);
1054 assert!(matches!(
1055 &mutations[0],
1056 Mutation::SetProp {
1057 property: PropertyKind::Text,
1058 value: PropertyValue::String(value),
1059 ..
1060 } if *value == "1"
1061 ));
1062 }
1063
1064 #[test]
1065 fn malformed_ffi_input_returns_protocol_error() {
1066 let mut output = MutationBatch::default();
1067 let status =
1069 unsafe { dioxus_compose_host_dispatch_event(std::ptr::null(), 4, &mut output) };
1070 assert_eq!(status, STATUS_PROTOCOL_ERROR);
1071 }
1072
1073 #[test]
1074 fn fr12_key_consumption_is_returned_and_does_not_leak() {
1075 let _launch = launch_guard();
1076 LaunchBuilder::new()
1077 .with_mode(LoopMode::Platform)
1078 .launch(key_app);
1079 let mut handshake = Vec::with_capacity(12);
1080 handshake.extend_from_slice(&SCHEMA_HASH.to_le_bytes());
1081 handshake.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
1082 handshake.extend_from_slice(&[LoopMode::Platform as u8, 0]);
1083 let mut output = MutationBatch::default();
1084 unsafe {
1086 assert_eq!(
1087 dioxus_compose_host_init(handshake.as_ptr(), handshake.len() as u32, &mut output,),
1088 STATUS_OK
1089 );
1090 }
1091 let initial = unsafe { std::slice::from_raw_parts(output.ptr, output.len as usize) };
1093 let handlers: Vec<_> = decode_batch(initial)
1094 .unwrap()
1095 .into_iter()
1096 .filter_map(|mutation| match mutation {
1097 Mutation::SetProp {
1098 node_id,
1099 property: PropertyKind::OnKeyDown,
1100 value: PropertyValue::Integer(handler),
1101 } => Some((node_id, handler as u64)),
1102 _ => None,
1103 })
1104 .collect();
1105 assert_eq!(handlers.len(), 2);
1106
1107 let dispatch = |node_id, handler_id, shift_key, output: &mut MutationBatch| {
1108 let event = HostEvent {
1109 node_id,
1110 handler_id,
1111 payload: EventPayload::KeyDown {
1112 key: Key::Enter,
1113 shift_key,
1114 ctrl_key: false,
1115 alt_key: false,
1116 meta_key: false,
1117 },
1118 };
1119 let mut bytes = Vec::new();
1120 crate::protocol::encode_event(&event, &mut bytes).unwrap();
1121 unsafe {
1123 assert_eq!(
1124 dioxus_compose_host_dispatch_event(bytes.as_ptr(), bytes.len() as u32, output,),
1125 STATUS_OK
1126 );
1127 }
1128 };
1129
1130 dispatch(handlers[0].0, handlers[0].1, false, &mut output);
1131 assert_ne!(output.result, 0);
1132 dispatch(handlers[0].0, handlers[0].1, true, &mut output);
1133 assert_eq!(output.result, 0);
1134 dispatch(handlers[1].0, handlers[1].1, false, &mut output);
1135 assert_eq!(output.result, 0);
1136 dispatch(handlers[0].0, handlers[0].1, false, &mut output);
1137 assert_ne!(output.result, 0);
1138 dioxus_compose_host_shutdown();
1139 }
1140
1141 #[test]
1145 fn pr3_init_runs_on_a_different_thread_than_launch() {
1146 let _launch = launch_guard();
1147 LaunchBuilder::new()
1148 .with_mode(LoopMode::Platform)
1149 .launch(app);
1150 let mut handshake = Vec::with_capacity(12);
1151 handshake.extend_from_slice(&SCHEMA_HASH.to_le_bytes());
1152 handshake.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
1153 handshake.extend_from_slice(&[LoopMode::Platform as u8, 0]);
1154 std::thread::spawn(move || {
1155 let mut output = MutationBatch::default();
1156 let status = unsafe {
1158 dioxus_compose_host_init(handshake.as_ptr(), handshake.len() as u32, &mut output)
1159 };
1160 assert_eq!(status, STATUS_OK, "init must work off the launch thread");
1161 assert!(output.len > 0, "init returns the initial tree batch");
1162 dioxus_compose_host_shutdown();
1163 })
1164 .join()
1165 .unwrap();
1166 }
1167
1168 #[test]
1171 fn fr14_initial_batch_opens_with_the_launched_theme() {
1172 let _launch = launch_guard();
1173 let first_record = |builder: LaunchBuilder| {
1174 builder.with_mode(LoopMode::Platform).launch(app);
1175 let mut handshake = Vec::with_capacity(12);
1176 handshake.extend_from_slice(&SCHEMA_HASH.to_le_bytes());
1177 handshake.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
1178 handshake.extend_from_slice(&[LoopMode::Platform as u8, 0]);
1179 std::thread::spawn(move || {
1180 let mut output = MutationBatch::default();
1181 let status = unsafe {
1183 dioxus_compose_host_init(
1184 handshake.as_ptr(),
1185 handshake.len() as u32,
1186 &mut output,
1187 )
1188 };
1189 assert_eq!(status, STATUS_OK);
1190 let batch = unsafe { std::slice::from_raw_parts(output.ptr, output.len as usize) };
1192 let first = decode_batch(batch).unwrap().into_iter().next().unwrap();
1193 dioxus_compose_host_shutdown();
1194 first
1195 })
1196 .join()
1197 .unwrap()
1198 };
1199
1200 assert_eq!(
1202 first_record(LaunchBuilder::new()),
1203 Mutation::SetTheme(Theme::adaptive(DesignSystem::Material3))
1204 );
1205 assert_eq!(
1206 first_record(LaunchBuilder::new().with_theme(Theme::unified(DesignSystem::Fluent))),
1207 Mutation::SetTheme(Theme {
1208 design_system: DesignSystem::Fluent,
1209 fallback: DesignSystem::Fluent,
1210 color_scheme: ColorScheme::FollowSystem,
1211 adaptive: false,
1212 fonts: [0; crate::schema::TYPE_ROLE_COUNT],
1213 })
1214 );
1215 assert_eq!(
1216 first_record(LaunchBuilder::new().with_theme(Theme::adaptive(DesignSystem::Cupertino))),
1217 Mutation::SetTheme(Theme {
1218 design_system: DesignSystem::Cupertino,
1219 fallback: DesignSystem::Cupertino,
1220 color_scheme: ColorScheme::FollowSystem,
1221 adaptive: true,
1222 fonts: [0; crate::schema::TYPE_ROLE_COUNT],
1223 })
1224 );
1225 }
1226
1227 #[test]
1230 fn fr14_set_theme_is_not_resent_every_frame() {
1231 let mut host = Host::new(app);
1232 let initial = decode_batch(host.rebuild().unwrap()).unwrap();
1233 assert!(matches!(initial.first(), Some(Mutation::SetTheme(_))));
1234 let frame = decode_batch(host.render_frame(0).unwrap()).unwrap();
1235 assert!(
1236 !frame
1237 .iter()
1238 .any(|mutation| matches!(mutation, Mutation::SetTheme(_)))
1239 );
1240 }
1241
1242 #[test]
1243 fn all_exports_isolate_invalid_calls() {
1244 let _launch = launch_guard();
1245 let mut output = MutationBatch::default();
1246 unsafe {
1248 assert_eq!(
1249 dioxus_compose_host_init(std::ptr::null(), 1, &mut output),
1250 STATUS_PROTOCOL_ERROR
1251 );
1252 assert_eq!(
1254 dioxus_compose_host_render_frame(0, &mut output),
1255 STATUS_NOT_INITIALIZED
1256 );
1257 dioxus_compose_host_release_batch(std::ptr::null_mut());
1258 }
1259 dioxus_compose_host_shutdown();
1260 }
1261}
1262
1263pub fn demo_theme() -> Theme {
1275 demo_theme_for(Theme::adaptive(crate::schema::DesignSystem::Material3))
1276}
1277
1278pub fn demo_theme_for(theme: Theme) -> Theme {
1287 use crate::schema::DesignSystem;
1288 let theme = match std::env::var("DXC_DESIGN").as_deref().map(str::trim) {
1289 Ok("material3") => Theme::unified(DesignSystem::Material3),
1290 Ok("cupertino") => Theme::unified(DesignSystem::Cupertino),
1291 Ok("fluent") => Theme::unified(DesignSystem::Fluent),
1292 Ok("gnome") => Theme::unified(DesignSystem::Gnome),
1293 Ok("breeze") => Theme::unified(DesignSystem::Breeze),
1294 Ok("deepin") => Theme::unified(DesignSystem::Deepin),
1295 Ok("liquidglass") | Ok("liquid-glass") => Theme::unified(DesignSystem::LiquidGlass),
1298 _ => return with_scheme_override(theme),
1301 }
1302 .with_color_scheme(theme.color_scheme);
1303 with_scheme_override(theme)
1304}
1305
1306fn with_scheme_override(theme: Theme) -> Theme {
1308 match std::env::var("DXC_SCHEME").as_deref().map(str::trim) {
1309 Ok("light") => theme.with_color_scheme(crate::schema::ColorScheme::Light),
1310 Ok("dark") => theme.with_color_scheme(crate::schema::ColorScheme::Dark),
1311 _ => theme,
1312 }
1313}
1314
1315#[cfg(test)]
1316mod demo_theme_tests {
1317 use super::*;
1318 use crate::schema::{ColorScheme, DESIGN_SYSTEM_SCHEMA, DesignSystem};
1319 use std::sync::{Mutex, MutexGuard};
1320
1321 static ENVIRONMENT: Mutex<()> = Mutex::new(());
1326
1327 fn exclusive_environment() -> MutexGuard<'static, ()> {
1329 let guard = ENVIRONMENT
1330 .lock()
1331 .unwrap_or_else(|error| error.into_inner());
1332 unsafe {
1335 std::env::remove_var("DXC_DESIGN");
1336 std::env::remove_var("DXC_SCHEME");
1337 }
1338 guard
1339 }
1340
1341 #[test]
1344 fn fr14_a_named_design_system_is_unified_and_anything_else_adapts() {
1345 let _environment = exclusive_environment();
1346 for (name, system) in [
1349 ("material3", DesignSystem::Material3),
1350 ("cupertino", DesignSystem::Cupertino),
1351 ("fluent", DesignSystem::Fluent),
1352 ("gnome", DesignSystem::Gnome),
1353 ("breeze", DesignSystem::Breeze),
1354 ("deepin", DesignSystem::Deepin),
1355 ("liquidglass", DesignSystem::LiquidGlass),
1356 ("liquid-glass", DesignSystem::LiquidGlass),
1357 ] {
1358 unsafe { std::env::set_var("DXC_DESIGN", name) };
1359 assert_eq!(
1360 demo_theme(),
1361 Theme::unified(system),
1362 "{name} is not selectable"
1363 );
1364 }
1365 assert_eq!(
1366 DESIGN_SYSTEM_SCHEMA.len(),
1367 7,
1368 "a system nobody can select goes unseen"
1369 );
1370
1371 unsafe { std::env::set_var("DXC_DESIGN", "nonsense") };
1372 assert_eq!(demo_theme(), Theme::adaptive(DesignSystem::Material3));
1373
1374 unsafe { std::env::remove_var("DXC_DESIGN") };
1375 assert_eq!(demo_theme(), Theme::adaptive(DesignSystem::Material3));
1376 }
1377
1378 #[test]
1381 fn fr14_a_named_colour_scheme_is_pinned_and_anything_else_is_left_alone() {
1382 let _environment = exclusive_environment();
1383 for (name, scheme) in [("light", ColorScheme::Light), ("dark", ColorScheme::Dark)] {
1385 unsafe { std::env::set_var("DXC_SCHEME", name) };
1386 assert_eq!(
1387 demo_theme().color_scheme,
1388 scheme,
1389 "{name} is not selectable"
1390 );
1391 }
1392
1393 unsafe { std::env::set_var("DXC_SCHEME", "nonsense") };
1394 assert_eq!(demo_theme().color_scheme, ColorScheme::FollowSystem);
1395
1396 unsafe { std::env::remove_var("DXC_SCHEME") };
1397 assert_eq!(demo_theme().color_scheme, ColorScheme::FollowSystem);
1398 }
1399
1400 #[test]
1404 fn fr14_a_samples_own_theme_survives_everything_the_variables_do_not_name() {
1405 let _environment = exclusive_environment();
1406 let sample = Theme::unified(DesignSystem::Cupertino).with_color_scheme(ColorScheme::Light);
1407 assert_eq!(demo_theme_for(sample), sample);
1408
1409 unsafe { std::env::set_var("DXC_DESIGN", "liquidglass") };
1410 assert_eq!(
1411 demo_theme_for(sample),
1412 Theme::unified(DesignSystem::LiquidGlass).with_color_scheme(ColorScheme::Light)
1413 );
1414
1415 unsafe { std::env::set_var("DXC_SCHEME", "dark") };
1416 assert_eq!(
1417 demo_theme_for(sample),
1418 Theme::unified(DesignSystem::LiquidGlass).with_color_scheme(ColorScheme::Dark)
1419 );
1420
1421 unsafe {
1422 std::env::remove_var("DXC_DESIGN");
1423 std::env::remove_var("DXC_SCHEME");
1424 };
1425 }
1426 #[test]
1433 fn pr2_a_handshake_from_another_schema_is_refused() {
1434 let mut wrong = Vec::with_capacity(12);
1435 wrong.extend_from_slice(&SCHEMA_HASH.wrapping_add(1).to_le_bytes());
1436 wrong.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
1437 wrong.extend_from_slice(&[LoopMode::Renderer as u8, 0]);
1438 assert!(
1439 matches!(
1440 parse_handshake(&wrong),
1441 Err(crate::protocol::ProtocolError::InvalidEnvelope)
1442 ),
1443 "a handshake carrying another schema's hash was accepted, so the two halves \
1444 would go on to disagree about what every record means"
1445 );
1446
1447 let mut right = Vec::with_capacity(12);
1448 right.extend_from_slice(&SCHEMA_HASH.to_le_bytes());
1449 right.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
1450 right.extend_from_slice(&[LoopMode::Renderer as u8, 0]);
1451 assert!(
1452 parse_handshake(&right).is_ok(),
1453 "the handshake this build generates is not one it accepts"
1454 );
1455 }
1456}
1457
1458#[used]
1470static BOUNDARY_EXPORTS: BoundaryExports = BoundaryExports([
1471 dioxus_compose_host_init as *const (),
1472 dioxus_compose_host_dispatch_event as *const (),
1473 dioxus_compose_host_render_frame as *const (),
1474 dioxus_compose_host_release_batch as *const (),
1475 dioxus_compose_host_shutdown as *const (),
1476]);
1477
1478#[cfg(all(target_os = "windows", target_env = "msvc"))]
1494const EXPORT_DIRECTIVES: &str = concat!(
1495 " /EXPORT:dioxus_compose_host_init",
1496 " /EXPORT:dioxus_compose_host_dispatch_event",
1497 " /EXPORT:dioxus_compose_host_render_frame",
1498 " /EXPORT:dioxus_compose_host_release_batch",
1499 " /EXPORT:dioxus_compose_host_shutdown",
1500);
1501
1502#[cfg(all(target_os = "windows", target_env = "msvc"))]
1503#[used]
1504#[unsafe(link_section = ".drectve")]
1505static EXPORT_DIRECTIVE_BYTES: [u8; EXPORT_DIRECTIVES.len()] = {
1506 let source = EXPORT_DIRECTIVES.as_bytes();
1507 let mut bytes = [0u8; EXPORT_DIRECTIVES.len()];
1508 let mut index = 0;
1509 while index < source.len() {
1510 bytes[index] = source[index];
1511 index += 1;
1512 }
1513 bytes
1514};
1515
1516struct BoundaryExports(#[allow(dead_code)] [*const (); 5]);
1518
1519unsafe impl Sync for BoundaryExports {}