use crate::protocol::{HostEvent, ProtocolError, decode_event};
use crate::renderer::ComposeRenderer;
use crate::schema::{
AssetKind, EventPayload, IconRole, LoopMode, PROTOCOL_VERSION, SCHEMA_HASH, Theme,
};
use crate::{Element, KeyEvent, RangeRequest, Selection, VirtualDom};
use dioxus_core::{ElementId, Event};
use std::cell::RefCell;
use std::ffi::c_int;
use std::future::Future;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::pin::pin;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::task::{Context, Poll, Wake, Waker};
pub const STATUS_OK: i32 = 0;
pub const STATUS_PROTOCOL_ERROR: i32 = -1;
pub const STATUS_NOT_INITIALIZED: i32 = -2;
pub const STATUS_ALREADY_INITIALIZED: i32 = -3;
pub const STATUS_PANIC: i32 = -4;
pub const STATUS_NO_RENDERER: i32 = -5;
const EXIT_FAILURE: i32 = 1;
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct MutationBatch {
pub ptr: *const u8,
pub len: u32,
pub result: i64,
}
impl Default for MutationBatch {
fn default() -> Self {
Self {
ptr: std::ptr::null(),
len: 0,
result: 0,
}
}
}
#[derive(Clone, Copy)]
pub struct RendererApi {
pub run: extern "C" fn() -> c_int,
pub request_frame: extern "C" fn(),
}
static RENDERER_API: OnceLock<RendererApi> = OnceLock::new();
static FRAME_REQUESTED: AtomicBool = AtomicBool::new(false);
static EVENT_DISPATCH_ACTIVE: AtomicBool = AtomicBool::new(false);
static DEFERRED_FRAME_REQUEST: AtomicBool = AtomicBool::new(false);
static LIFECYCLE_SUPPRESSED: AtomicBool = AtomicBool::new(false);
pub fn install_renderer_api(api: RendererApi) -> Result<(), RendererApi> {
RENDERER_API.set(api)
}
#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
unsafe extern "C" {
fn dioxus_compose_renderer_run() -> c_int;
fn dioxus_compose_renderer_request_frame();
}
#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
extern "C" fn native_run() -> c_int {
unsafe { dioxus_compose_renderer_run() }
}
#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
extern "C" fn native_request_frame() {
unsafe { dioxus_compose_renderer_request_frame() }
}
#[cfg(any(test, feature = "mock-renderer"))]
extern "C" fn native_run() -> c_int {
STATUS_OK
}
#[cfg(any(test, feature = "mock-renderer"))]
extern "C" fn native_request_frame() {}
#[cfg(all(not(renderer_linked), not(any(test, feature = "mock-renderer"))))]
extern "C" fn native_run() -> c_int {
eprintln!("{}", no_renderer_message());
STATUS_NO_RENDERER
}
#[cfg(all(not(renderer_linked), not(any(test, feature = "mock-renderer"))))]
extern "C" fn native_request_frame() {}
pub fn no_renderer_message() -> String {
format!(
"dioxus-compose: this application was built without a renderer, so there is nothing\n\
to draw with and nothing to draw on. Exiting {EXIT_FAILURE} rather than looking like\n\
a program that ran and finished.\n\
\n\
A default `cargo build` links the renderer for the platform it is building for and\n\
downloads it if it has to. A build reaches this message by turning that off:\n\
\n\
\x20 default-features = false, without re-enabling `native-renderer`\n\
\x20 a documentation build, which has no network to fetch a renderer with\n\
\n\
Building for a test or with the `mock-renderer` feature is a third way, and that\n\
one is silent on purpose: those builds drive the boundary directly and want no\n\
window."
)
}
fn renderer_api() -> RendererApi {
RENDERER_API.get().copied().unwrap_or(RendererApi {
run: native_run,
request_frame: native_request_frame,
})
}
pub fn request_frame_from_worker() {
if !FRAME_REQUESTED.swap(true, Ordering::AcqRel) {
if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire)
|| EVENT_DISPATCH_ACTIVE.load(Ordering::Acquire)
{
DEFERRED_FRAME_REQUEST.store(true, Ordering::Release);
} else {
(renderer_api().request_frame)();
FRAME_REQUESTED.store(false, Ordering::Release);
}
}
}
struct EventDispatchGuard;
impl EventDispatchGuard {
fn enter() -> Self {
EVENT_DISPATCH_ACTIVE.store(true, Ordering::Release);
Self
}
}
impl Drop for EventDispatchGuard {
fn drop(&mut self) {
EVENT_DISPATCH_ACTIVE.store(false, Ordering::Release);
if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire) {
return;
}
if DEFERRED_FRAME_REQUEST.swap(false, Ordering::AcqRel) {
(renderer_api().request_frame)();
FRAME_REQUESTED.store(false, Ordering::Release);
}
}
}
struct FrameWake;
impl Wake for FrameWake {
fn wake(self: Arc<Self>) {
request_frame_from_worker();
}
fn wake_by_ref(self: &Arc<Self>) {
request_frame_from_worker();
}
}
struct PendingAppend {
node_id: u32,
text: String,
dirty: bool,
}
pub struct Host {
app: fn() -> Element,
theme: Theme,
window: crate::schema::Window,
dom: VirtualDom,
renderer: ComposeRenderer,
frame_waker: Waker,
pending_appends: Vec<PendingAppend>,
}
impl Host {
pub fn new(app: fn() -> Element) -> Self {
Self::with_theme(app, launched_theme())
}
pub fn with_theme(app: fn() -> Element, theme: Theme) -> Self {
crate::window::reset_window_size();
crate::message::reset_messages();
crate::asset::requeue_all();
Self {
app,
theme,
window: launched_window(),
dom: VirtualDom::new(app),
renderer: ComposeRenderer::new(),
frame_waker: Waker::from(Arc::new(FrameWake)),
pending_appends: Vec::new(),
}
}
pub fn rebuild(&mut self) -> Result<&[u8], ProtocolError> {
self.renderer.begin_frame();
self.renderer.set_theme(self.theme);
self.renderer.set_window(self.window);
self.dom.rebuild(&mut self.renderer);
self.flush_messages();
self.arm_scheduler_wake();
self.renderer.finish_frame()
}
pub fn dispatch_event(&mut self, bytes: &[u8]) -> Result<(&[u8], i64), ProtocolError> {
let event = decode_event(bytes)?;
self.dispatch(event)
}
pub fn arena(&self) -> (*const u8, usize) {
self.renderer.arena()
}
pub fn resync(&mut self) -> Result<(&[u8], i64), ProtocolError> {
*self = Self::with_theme(self.app, self.theme);
Ok((self.rebuild()?, 0))
}
fn set_lifecycle_running(&mut self, running: bool) -> Result<(&[u8], i64), ProtocolError> {
LIFECYCLE_SUPPRESSED.store(!running, Ordering::Release);
if running {
if DEFERRED_FRAME_REQUEST.swap(false, Ordering::AcqRel) {
(renderer_api().request_frame)();
}
FRAME_REQUESTED.store(false, Ordering::Release);
}
self.renderer.begin_frame();
Ok((self.renderer.finish_frame()?, 0))
}
fn publish_design_system(
&mut self,
system: crate::schema::DesignSystem,
) -> Result<(&[u8], i64), ProtocolError> {
if crate::design::publish(system) {
self.renderer.begin_frame();
self.dom.render_immediate(&mut self.renderer);
self.flush_messages();
self.arm_scheduler_wake();
return Ok((self.renderer.finish_frame()?, 0));
}
self.renderer.begin_frame();
Ok((self.renderer.finish_frame()?, 0))
}
pub fn dispatch(&mut self, event: HostEvent<'_>) -> Result<(&[u8], i64), ProtocolError> {
match event.payload {
EventPayload::Resync => return self.resync(),
EventPayload::DesignSystemResolved(system) => {
return self.publish_design_system(system);
}
EventPayload::LifecycleStart => return self.set_lifecycle_running(true),
EventPayload::LifecycleStop => return self.set_lifecycle_running(false),
_ => {}
}
if let EventPayload::WindowSizeChanged {
width_dp,
height_dp,
class,
height_class,
} = event.payload
{
let size = crate::window::WindowSize::new(width_dp, height_dp, class, height_class);
if event.node_id != 0 {
let woke = self
.renderer
.size_token(event.node_id)
.is_some_and(|token| crate::window::publish_node(token, size));
if !woke {
self.renderer.begin_frame();
return Ok((self.renderer.finish_frame()?, 0));
}
self.renderer.begin_frame();
self.dom.render_immediate(&mut self.renderer);
self.flush_messages();
self.arm_scheduler_wake();
return Ok((self.renderer.finish_frame()?, 0));
}
crate::window::publish(size);
self.renderer.begin_frame();
self.dom.render_immediate(&mut self.renderer);
self.flush_messages();
self.arm_scheduler_wake();
return Ok((self.renderer.finish_frame()?, 0));
}
let message_action = match event.payload {
EventPayload::Clicked => crate::message::take_action(event.handler_id),
_ => None,
};
if let Some(action) = message_action {
if event.node_id != 0 {
return Err(ProtocolError::InvalidValueKind(0));
}
let _dispatch_guard = EventDispatchGuard::enter();
action.call(());
self.renderer.begin_frame();
self.dom.render_immediate(&mut self.renderer);
self.flush_messages();
self.arm_scheduler_wake();
return Ok((self.renderer.finish_frame()?, 0));
}
let Some((element, node_id, name)) = self.renderer.handler(event.handler_id) else {
return Err(ProtocolError::InvalidValueKind(0));
};
if event.node_id != node_id {
return Err(ProtocolError::InvalidValueKind(0));
}
let mut key_event = None;
let event_data = match event.payload {
EventPayload::Clicked | EventPayload::FocusLost => {
Event::new(Rc::new(()), true).into_any()
}
EventPayload::TextChanged(text) | EventPayload::TextSubmitted(text) => {
Event::new(Rc::new(text.to_owned()), true).into_any()
}
EventPayload::ProtocolError { .. } => {
return Err(ProtocolError::InvalidValueKind(0));
}
EventPayload::KeyDown {
key,
shift_key,
ctrl_key,
alt_key,
meta_key,
} => {
let value = KeyEvent::new(key, shift_key, ctrl_key, alt_key, meta_key);
key_event = Some(value.clone());
Event::new(Rc::new(value), true).into_any()
}
EventPayload::FilesEntered => Event::new(Rc::new(()), true).into_any(),
EventPayload::FilesDropped(paths) => {
Event::new(Rc::new(crate::widgets::FileDrop::new(paths)), true).into_any()
}
EventPayload::RangeRequested { start, count } => {
Event::new(Rc::new(RangeRequest::new(start, count)), true).into_any()
}
EventPayload::ValueChanged(value) => Event::new(Rc::new(value), true).into_any(),
EventPayload::WindowSizeChanged { .. }
| EventPayload::DesignSystemResolved(_)
| EventPayload::Resync
| EventPayload::LifecycleStart
| EventPayload::LifecycleStop => unreachable!("handled above"),
};
let _dispatch_guard = EventDispatchGuard::enter();
self.dom.runtime().handle_event(name, event_data, element);
self.renderer.begin_frame();
self.dom.render_immediate(&mut self.renderer);
self.flush_messages();
self.arm_scheduler_wake();
let result = i64::from(key_event.is_some_and(|event| event.consumed()));
Ok((self.renderer.finish_frame()?, result))
}
pub fn render_frame(&mut self, _frame_time_nanos: u64) -> Result<&[u8], ProtocolError> {
FRAME_REQUESTED.store(false, Ordering::Release);
EVENT_DISPATCH_ACTIVE.store(false, Ordering::Release);
DEFERRED_FRAME_REQUEST.store(false, Ordering::Release);
self.renderer.begin_frame();
self.dom.render_immediate(&mut self.renderer);
self.flush_pending_appends();
self.flush_messages();
self.arm_scheduler_wake();
self.renderer.finish_frame()
}
pub fn append_text(&mut self, node_id: u32, tail: &str) {
match self
.pending_appends
.iter_mut()
.find(|pending| pending.node_id == node_id)
{
Some(pending) => {
pending.text.push_str(tail);
pending.dirty = true;
}
None => self.pending_appends.push(PendingAppend {
node_id,
text: tail.to_owned(),
dirty: true,
}),
}
request_frame_from_worker();
}
fn flush_messages(&mut self) {
let renderer = &mut self.renderer;
crate::asset::drain(|pending| {
renderer.register_asset(pending.asset_id, pending.kind, pending.bytes);
});
crate::message::drain(|message| {
renderer.show_message(
message.handler_id,
&message.text,
&message.action,
message.duration,
);
});
}
fn flush_pending_appends(&mut self) {
for index in 0..self.pending_appends.len() {
let pending = &self.pending_appends[index];
if pending.dirty {
self.renderer
.append_text_node(pending.node_id, &pending.text);
}
}
for pending in &mut self.pending_appends {
pending.text.clear();
pending.dirty = false;
}
}
pub fn register_asset(
&mut self,
asset_id: u32,
kind: AssetKind,
bytes: &[u8],
) -> Result<&[u8], ProtocolError> {
self.renderer.begin_frame();
self.renderer.register_asset(asset_id, kind, bytes);
self.renderer.finish_frame()
}
pub fn register_icon(&mut self, asset_id: u32, role: IconRole) -> Result<&[u8], ProtocolError> {
self.register_asset(
asset_id,
AssetKind::VectorIcon,
&(role as u16).to_le_bytes(),
)
}
pub fn release_asset(&mut self, asset_id: u32) -> Result<&[u8], ProtocolError> {
self.renderer.begin_frame();
self.renderer.release_asset(asset_id);
self.renderer.finish_frame()
}
pub fn set_text(
&mut self,
node_id: u32,
text: &str,
selection: Option<Selection>,
) -> Result<&[u8], ProtocolError> {
self.renderer.begin_frame();
self.renderer.set_text_node(node_id, text, selection);
self.renderer.finish_frame()
}
pub fn handler_target(&self, handler_id: u64) -> Option<ElementId> {
self.renderer.handler(handler_id).map(|value| value.0)
}
fn arm_scheduler_wake(&mut self) {
let mut context = Context::from_waker(&self.frame_waker);
let future = self.dom.wait_for_work();
let mut future = pin!(future);
if future.as_mut().poll(&mut context) == Poll::Ready(()) {
request_frame_from_worker();
}
}
}
struct HostSlot(RefCell<Option<Host>>);
impl Drop for HostSlot {
fn drop(&mut self) {
std::mem::forget(self.0.borrow_mut().take());
}
}
impl std::ops::Deref for HostSlot {
type Target = RefCell<Option<Host>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
static APP: Mutex<Option<fn() -> Element>> = Mutex::new(None);
static THEME: Mutex<Theme> = Mutex::new(Theme::unified(crate::schema::DesignSystem::Material3));
static WINDOW: Mutex<crate::schema::Window> = Mutex::new(crate::schema::Window::new());
thread_local! {
static HOST: HostSlot = const { HostSlot(RefCell::new(None)) };
}
fn launched_app() -> Option<fn() -> Element> {
APP.lock().map_or(None, |app| *app)
}
fn launched_window() -> crate::schema::Window {
WINDOW
.lock()
.map_or_else(|error| *error.into_inner(), |window| *window)
}
fn launched_theme() -> Theme {
THEME
.lock()
.map_or_else(|error| *error.into_inner(), |theme| *theme)
}
#[derive(Clone, Copy, Debug)]
pub struct LaunchBuilder {
mode: LoopMode,
theme: Theme,
window: crate::schema::Window,
}
impl Default for LaunchBuilder {
fn default() -> Self {
Self {
mode: LoopMode::Renderer,
theme: Theme::default(),
window: crate::schema::Window::new(),
}
}
}
impl LaunchBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_mode(mut self, mode: LoopMode) -> Self {
self.mode = mode;
self
}
pub fn with_theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
pub fn with_window(mut self, window: crate::schema::Window) -> Self {
self.window = window;
self
}
pub fn launch(self, app: fn() -> Element) {
let status = self.try_launch(app);
if status != STATUS_OK {
std::process::exit(EXIT_FAILURE);
}
}
pub fn try_launch(self, app: fn() -> Element) -> i32 {
if let Ok(mut slot) = APP.lock() {
*slot = Some(app);
}
if let Ok(mut slot) = THEME.lock() {
*slot = self.theme;
}
if let Ok(mut slot) = WINDOW.lock() {
*slot = self.window;
}
if self.mode != LoopMode::Renderer {
return STATUS_OK;
}
(renderer_api().run)()
}
}
pub fn launch(app: fn() -> Element) {
LaunchBuilder::new().launch(app);
}
fn parse_handshake(bytes: &[u8]) -> Result<LoopMode, ProtocolError> {
if bytes.len() < 12 {
return Err(ProtocolError::Truncated);
}
let hash = u64::from_le_bytes(
bytes[0..8]
.try_into()
.map_err(|_| ProtocolError::Truncated)?,
);
let version = u16::from_le_bytes(
bytes[8..10]
.try_into()
.map_err(|_| ProtocolError::Truncated)?,
);
if hash != SCHEMA_HASH || version != PROTOCOL_VERSION {
eprintln!(
"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."
);
return Err(ProtocolError::InvalidEnvelope);
}
match bytes[10] {
0 => Ok(LoopMode::Renderer),
1 => Ok(LoopMode::Platform),
other => Err(ProtocolError::InvalidValueKind(u16::from(other))),
}
}
unsafe fn input_slice<'a>(ptr: *const u8, len: u32) -> Result<&'a [u8], ProtocolError> {
if len == 0 {
return Ok(&[]);
}
if ptr.is_null() {
return Err(ProtocolError::Truncated);
}
Ok(unsafe { std::slice::from_raw_parts(ptr, len as usize) })
}
unsafe fn write_batch(
out: *mut MutationBatch,
bytes: &[u8],
result: i64,
) -> Result<(), ProtocolError> {
if out.is_null() {
return Err(ProtocolError::Truncated);
}
let len = u32::try_from(bytes.len()).map_err(|_| ProtocolError::LengthOverflow)?;
unsafe {
out.write(MutationBatch {
ptr: bytes.as_ptr(),
len,
result,
});
}
Ok(())
}
fn ffi_status(operation: impl FnOnce() -> Result<(), i32>) -> i32 {
match catch_unwind(AssertUnwindSafe(operation)) {
Ok(Ok(())) => STATUS_OK,
Ok(Err(status)) => status,
Err(_) => STATUS_PANIC,
}
}
fn protocol_error(_error: ProtocolError) -> i32 {
STATUS_PROTOCOL_ERROR
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dioxus_compose_host_init(
handshake: *const u8,
len: u32,
out: *mut MutationBatch,
) -> i32 {
ffi_status(|| {
let bytes = unsafe { input_slice(handshake, len) }.map_err(protocol_error)?;
let _mode = parse_handshake(bytes).map_err(protocol_error)?;
let app = launched_app().ok_or(STATUS_NOT_INITIALIZED)?;
HOST.with(|slot| {
let mut host_slot = slot.borrow_mut();
if host_slot.is_some() {
return Err(STATUS_ALREADY_INITIALIZED);
}
let mut host = Host::new(app);
let batch = host.rebuild().map_err(protocol_error)?;
unsafe { write_batch(out, batch, 0) }.map_err(protocol_error)?;
*host_slot = Some(host);
Ok(())
})
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dioxus_compose_host_dispatch_event(
event: *const u8,
len: u32,
out: *mut MutationBatch,
) -> i32 {
ffi_status(|| {
let bytes = unsafe { input_slice(event, len) }.map_err(protocol_error)?;
HOST.with(|slot| {
let mut host_slot = slot.borrow_mut();
let host = host_slot.as_mut().ok_or(STATUS_NOT_INITIALIZED)?;
let (batch, result) = host.dispatch_event(bytes).map_err(protocol_error)?;
unsafe { write_batch(out, batch, result) }.map_err(protocol_error)
})
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dioxus_compose_host_render_frame(
frame_time_nanos: u64,
out: *mut MutationBatch,
) -> i32 {
ffi_status(|| {
HOST.with(|slot| {
let mut host_slot = slot.borrow_mut();
let host = host_slot.as_mut().ok_or(STATUS_NOT_INITIALIZED)?;
let batch = host
.render_frame(frame_time_nanos)
.map_err(protocol_error)?;
unsafe { write_batch(out, batch, 0) }.map_err(protocol_error)
})
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dioxus_compose_host_release_batch(batch: *mut MutationBatch) {
let _ = catch_unwind(AssertUnwindSafe(|| {
if !batch.is_null() {
unsafe { batch.write(MutationBatch::default()) };
}
}));
}
pub fn current_arena() -> (*const u8, usize) {
HOST.with(|slot| {
slot.borrow()
.as_ref()
.map_or((std::ptr::null(), 0), Host::arena)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn dioxus_compose_host_shutdown() {
let _ = catch_unwind(AssertUnwindSafe(|| {
HOST.with(|slot| *slot.borrow_mut() = None);
FRAME_REQUESTED.store(false, Ordering::Release);
}));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use crate::protocol::{Mutation, PropertyValue, decode_batch};
use crate::schema::PropertyKind;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
static CLICKS: AtomicUsize = AtomicUsize::new(0);
static LAUNCH: Mutex<()> = Mutex::new(());
fn launch_guard() -> std::sync::MutexGuard<'static, ()> {
LAUNCH
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn app() -> Element {
let mut count = use_signal(|| 0_i64);
rsx! {
Column {
Text { text: count().to_string() }
TextField { placeholder: "Message" }
Button {
text: "Increment",
on_click: move |_| {
CLICKS.fetch_add(1, Ordering::SeqCst);
*count.write() += 1;
}
}
}
}
}
fn key_app() -> Element {
rsx! {
Column {
TextField {
multiline: true,
on_key_down: move |event: KeyEvent| {
if event.key() == Key::Enter && !event.shift_key() {
event.consume();
}
}
}
TextField {
on_key_down: move |_event: KeyEvent| {}
}
}
}
}
#[derive(Default)]
struct MockRenderer {
widgets: HashMap<u32, crate::WidgetKind>,
parents: HashMap<u32, u32>,
}
impl MockRenderer {
fn apply(&mut self, batch: &[Mutation<'_>]) {
for mutation in batch {
match mutation {
Mutation::Create { node_id, widget } => {
self.widgets.insert(*node_id, *widget);
}
Mutation::Insert {
parent_id, node_id, ..
}
| Mutation::Move {
parent_id, node_id, ..
} => {
self.parents.insert(*node_id, *parent_id);
}
Mutation::Remove { node_id } => {
self.widgets.remove(node_id);
self.parents.remove(node_id);
}
Mutation::SetProp { .. }
| Mutation::SetModifier { .. }
| Mutation::SetText { .. }
| Mutation::AppendText { .. }
| Mutation::RegisterAsset { .. }
| Mutation::ReleaseAsset { .. }
| Mutation::ShowMessage { .. }
| Mutation::SetTheme(_)
| Mutation::SetWindow(_) => {}
}
}
}
}
#[test]
fn click_runs_once_and_emits_only_text_set_prop() {
CLICKS.store(0, Ordering::SeqCst);
let mut host = Host::new(app);
let initial = decode_batch(host.rebuild().unwrap()).unwrap();
let mut mock = MockRenderer::default();
mock.apply(&initial);
assert_eq!(mock.widgets.len(), 4);
assert_eq!(mock.parents.len(), 3);
let (button_node, handler_id) = initial
.iter()
.find_map(|mutation| match mutation {
Mutation::SetProp {
node_id,
property: PropertyKind::OnClick,
value: PropertyValue::Integer(handler),
} => Some((*node_id, *handler as u64)),
_ => None,
})
.unwrap();
drop(initial);
let event = HostEvent {
node_id: button_node,
handler_id,
payload: EventPayload::Clicked,
};
let (batch, result) = host.dispatch(event).unwrap();
let mutations = decode_batch(batch).unwrap();
assert_eq!(CLICKS.load(Ordering::SeqCst), 1);
assert_eq!(result, 0);
assert_eq!(mutations.len(), 1);
assert!(matches!(
&mutations[0],
Mutation::SetProp {
property: PropertyKind::Text,
value: PropertyValue::String(value),
..
} if *value == "1"
));
}
#[test]
fn malformed_ffi_input_returns_protocol_error() {
let mut output = MutationBatch::default();
let status =
unsafe { dioxus_compose_host_dispatch_event(std::ptr::null(), 4, &mut output) };
assert_eq!(status, STATUS_PROTOCOL_ERROR);
}
#[test]
fn fr12_key_consumption_is_returned_and_does_not_leak() {
let _launch = launch_guard();
LaunchBuilder::new()
.with_mode(LoopMode::Platform)
.launch(key_app);
let mut handshake = Vec::with_capacity(12);
handshake.extend_from_slice(&SCHEMA_HASH.to_le_bytes());
handshake.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
handshake.extend_from_slice(&[LoopMode::Platform as u8, 0]);
let mut output = MutationBatch::default();
unsafe {
assert_eq!(
dioxus_compose_host_init(handshake.as_ptr(), handshake.len() as u32, &mut output,),
STATUS_OK
);
}
let initial = unsafe { std::slice::from_raw_parts(output.ptr, output.len as usize) };
let handlers: Vec<_> = decode_batch(initial)
.unwrap()
.into_iter()
.filter_map(|mutation| match mutation {
Mutation::SetProp {
node_id,
property: PropertyKind::OnKeyDown,
value: PropertyValue::Integer(handler),
} => Some((node_id, handler as u64)),
_ => None,
})
.collect();
assert_eq!(handlers.len(), 2);
let dispatch = |node_id, handler_id, shift_key, output: &mut MutationBatch| {
let event = HostEvent {
node_id,
handler_id,
payload: EventPayload::KeyDown {
key: Key::Enter,
shift_key,
ctrl_key: false,
alt_key: false,
meta_key: false,
},
};
let mut bytes = Vec::new();
crate::protocol::encode_event(&event, &mut bytes).unwrap();
unsafe {
assert_eq!(
dioxus_compose_host_dispatch_event(bytes.as_ptr(), bytes.len() as u32, output,),
STATUS_OK
);
}
};
dispatch(handlers[0].0, handlers[0].1, false, &mut output);
assert_ne!(output.result, 0);
dispatch(handlers[0].0, handlers[0].1, true, &mut output);
assert_eq!(output.result, 0);
dispatch(handlers[1].0, handlers[1].1, false, &mut output);
assert_eq!(output.result, 0);
dispatch(handlers[0].0, handlers[0].1, false, &mut output);
assert_ne!(output.result, 0);
dioxus_compose_host_shutdown();
}
#[test]
fn pr3_init_runs_on_a_different_thread_than_launch() {
let _launch = launch_guard();
LaunchBuilder::new()
.with_mode(LoopMode::Platform)
.launch(app);
let mut handshake = Vec::with_capacity(12);
handshake.extend_from_slice(&SCHEMA_HASH.to_le_bytes());
handshake.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
handshake.extend_from_slice(&[LoopMode::Platform as u8, 0]);
std::thread::spawn(move || {
let mut output = MutationBatch::default();
let status = unsafe {
dioxus_compose_host_init(handshake.as_ptr(), handshake.len() as u32, &mut output)
};
assert_eq!(status, STATUS_OK, "init must work off the launch thread");
assert!(output.len > 0, "init returns the initial tree batch");
dioxus_compose_host_shutdown();
})
.join()
.unwrap();
}
#[test]
fn fr14_initial_batch_opens_with_the_launched_theme() {
let _launch = launch_guard();
let first_record = |builder: LaunchBuilder| {
builder.with_mode(LoopMode::Platform).launch(app);
let mut handshake = Vec::with_capacity(12);
handshake.extend_from_slice(&SCHEMA_HASH.to_le_bytes());
handshake.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
handshake.extend_from_slice(&[LoopMode::Platform as u8, 0]);
std::thread::spawn(move || {
let mut output = MutationBatch::default();
let status = unsafe {
dioxus_compose_host_init(
handshake.as_ptr(),
handshake.len() as u32,
&mut output,
)
};
assert_eq!(status, STATUS_OK);
let batch = unsafe { std::slice::from_raw_parts(output.ptr, output.len as usize) };
let first = decode_batch(batch).unwrap().into_iter().next().unwrap();
dioxus_compose_host_shutdown();
first
})
.join()
.unwrap()
};
assert_eq!(
first_record(LaunchBuilder::new()),
Mutation::SetTheme(Theme::adaptive(DesignSystem::Material3))
);
assert_eq!(
first_record(LaunchBuilder::new().with_theme(Theme::unified(DesignSystem::Fluent))),
Mutation::SetTheme(Theme {
design_system: DesignSystem::Fluent,
fallback: DesignSystem::Fluent,
color_scheme: ColorScheme::FollowSystem,
adaptive: false,
fonts: [0; crate::schema::TYPE_ROLE_COUNT],
})
);
assert_eq!(
first_record(LaunchBuilder::new().with_theme(Theme::adaptive(DesignSystem::Cupertino))),
Mutation::SetTheme(Theme {
design_system: DesignSystem::Cupertino,
fallback: DesignSystem::Cupertino,
color_scheme: ColorScheme::FollowSystem,
adaptive: true,
fonts: [0; crate::schema::TYPE_ROLE_COUNT],
})
);
}
#[test]
fn fr14_set_theme_is_not_resent_every_frame() {
let mut host = Host::new(app);
let initial = decode_batch(host.rebuild().unwrap()).unwrap();
assert!(matches!(initial.first(), Some(Mutation::SetTheme(_))));
let frame = decode_batch(host.render_frame(0).unwrap()).unwrap();
assert!(
!frame
.iter()
.any(|mutation| matches!(mutation, Mutation::SetTheme(_)))
);
}
#[test]
fn all_exports_isolate_invalid_calls() {
let _launch = launch_guard();
let mut output = MutationBatch::default();
unsafe {
assert_eq!(
dioxus_compose_host_init(std::ptr::null(), 1, &mut output),
STATUS_PROTOCOL_ERROR
);
assert_eq!(
dioxus_compose_host_render_frame(0, &mut output),
STATUS_NOT_INITIALIZED
);
dioxus_compose_host_release_batch(std::ptr::null_mut());
}
dioxus_compose_host_shutdown();
}
}
pub fn demo_theme() -> Theme {
demo_theme_for(Theme::adaptive(crate::schema::DesignSystem::Material3))
}
pub fn demo_theme_for(theme: Theme) -> Theme {
use crate::schema::DesignSystem;
let theme = match std::env::var("DXC_DESIGN").as_deref().map(str::trim) {
Ok("material3") => Theme::unified(DesignSystem::Material3),
Ok("cupertino") => Theme::unified(DesignSystem::Cupertino),
Ok("fluent") => Theme::unified(DesignSystem::Fluent),
Ok("gnome") => Theme::unified(DesignSystem::Gnome),
Ok("breeze") => Theme::unified(DesignSystem::Breeze),
Ok("deepin") => Theme::unified(DesignSystem::Deepin),
Ok("liquidglass") | Ok("liquid-glass") => Theme::unified(DesignSystem::LiquidGlass),
_ => return with_scheme_override(theme),
}
.with_color_scheme(theme.color_scheme);
with_scheme_override(theme)
}
fn with_scheme_override(theme: Theme) -> Theme {
match std::env::var("DXC_SCHEME").as_deref().map(str::trim) {
Ok("light") => theme.with_color_scheme(crate::schema::ColorScheme::Light),
Ok("dark") => theme.with_color_scheme(crate::schema::ColorScheme::Dark),
_ => theme,
}
}
#[cfg(test)]
mod demo_theme_tests {
use super::*;
use crate::schema::{ColorScheme, DESIGN_SYSTEM_SCHEMA, DesignSystem};
use std::sync::{Mutex, MutexGuard};
static ENVIRONMENT: Mutex<()> = Mutex::new(());
fn exclusive_environment() -> MutexGuard<'static, ()> {
let guard = ENVIRONMENT
.lock()
.unwrap_or_else(|error| error.into_inner());
unsafe {
std::env::remove_var("DXC_DESIGN");
std::env::remove_var("DXC_SCHEME");
}
guard
}
#[test]
fn fr14_a_named_design_system_is_unified_and_anything_else_adapts() {
let _environment = exclusive_environment();
for (name, system) in [
("material3", DesignSystem::Material3),
("cupertino", DesignSystem::Cupertino),
("fluent", DesignSystem::Fluent),
("gnome", DesignSystem::Gnome),
("breeze", DesignSystem::Breeze),
("deepin", DesignSystem::Deepin),
("liquidglass", DesignSystem::LiquidGlass),
("liquid-glass", DesignSystem::LiquidGlass),
] {
unsafe { std::env::set_var("DXC_DESIGN", name) };
assert_eq!(
demo_theme(),
Theme::unified(system),
"{name} is not selectable"
);
}
assert_eq!(
DESIGN_SYSTEM_SCHEMA.len(),
7,
"a system nobody can select goes unseen"
);
unsafe { std::env::set_var("DXC_DESIGN", "nonsense") };
assert_eq!(demo_theme(), Theme::adaptive(DesignSystem::Material3));
unsafe { std::env::remove_var("DXC_DESIGN") };
assert_eq!(demo_theme(), Theme::adaptive(DesignSystem::Material3));
}
#[test]
fn fr14_a_named_colour_scheme_is_pinned_and_anything_else_is_left_alone() {
let _environment = exclusive_environment();
for (name, scheme) in [("light", ColorScheme::Light), ("dark", ColorScheme::Dark)] {
unsafe { std::env::set_var("DXC_SCHEME", name) };
assert_eq!(
demo_theme().color_scheme,
scheme,
"{name} is not selectable"
);
}
unsafe { std::env::set_var("DXC_SCHEME", "nonsense") };
assert_eq!(demo_theme().color_scheme, ColorScheme::FollowSystem);
unsafe { std::env::remove_var("DXC_SCHEME") };
assert_eq!(demo_theme().color_scheme, ColorScheme::FollowSystem);
}
#[test]
fn fr14_a_samples_own_theme_survives_everything_the_variables_do_not_name() {
let _environment = exclusive_environment();
let sample = Theme::unified(DesignSystem::Cupertino).with_color_scheme(ColorScheme::Light);
assert_eq!(demo_theme_for(sample), sample);
unsafe { std::env::set_var("DXC_DESIGN", "liquidglass") };
assert_eq!(
demo_theme_for(sample),
Theme::unified(DesignSystem::LiquidGlass).with_color_scheme(ColorScheme::Light)
);
unsafe { std::env::set_var("DXC_SCHEME", "dark") };
assert_eq!(
demo_theme_for(sample),
Theme::unified(DesignSystem::LiquidGlass).with_color_scheme(ColorScheme::Dark)
);
unsafe {
std::env::remove_var("DXC_DESIGN");
std::env::remove_var("DXC_SCHEME");
};
}
#[test]
fn pr2_a_handshake_from_another_schema_is_refused() {
let mut wrong = Vec::with_capacity(12);
wrong.extend_from_slice(&SCHEMA_HASH.wrapping_add(1).to_le_bytes());
wrong.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
wrong.extend_from_slice(&[LoopMode::Renderer as u8, 0]);
assert!(
matches!(
parse_handshake(&wrong),
Err(crate::protocol::ProtocolError::InvalidEnvelope)
),
"a handshake carrying another schema's hash was accepted, so the two halves \
would go on to disagree about what every record means"
);
let mut right = Vec::with_capacity(12);
right.extend_from_slice(&SCHEMA_HASH.to_le_bytes());
right.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
right.extend_from_slice(&[LoopMode::Renderer as u8, 0]);
assert!(
parse_handshake(&right).is_ok(),
"the handshake this build generates is not one it accepts"
);
}
}
#[used]
static BOUNDARY_EXPORTS: BoundaryExports = BoundaryExports([
dioxus_compose_host_init as *const (),
dioxus_compose_host_dispatch_event as *const (),
dioxus_compose_host_render_frame as *const (),
dioxus_compose_host_release_batch as *const (),
dioxus_compose_host_shutdown as *const (),
]);
#[cfg(all(target_os = "windows", target_env = "msvc"))]
const EXPORT_DIRECTIVES: &str = concat!(
" /EXPORT:dioxus_compose_host_init",
" /EXPORT:dioxus_compose_host_dispatch_event",
" /EXPORT:dioxus_compose_host_render_frame",
" /EXPORT:dioxus_compose_host_release_batch",
" /EXPORT:dioxus_compose_host_shutdown",
);
#[cfg(all(target_os = "windows", target_env = "msvc"))]
#[used]
#[unsafe(link_section = ".drectve")]
static EXPORT_DIRECTIVE_BYTES: [u8; EXPORT_DIRECTIVES.len()] = {
let source = EXPORT_DIRECTIVES.as_bytes();
let mut bytes = [0u8; EXPORT_DIRECTIVES.len()];
let mut index = 0;
while index < source.len() {
bytes[index] = source[index];
index += 1;
}
bytes
};
struct BoundaryExports(#[allow(dead_code)] [*const (); 5]);
unsafe impl Sync for BoundaryExports {}