use crate::Selection;
use crate::protocol::{HostEvent, ProtocolError, decode_event};
use crate::runtime::Runtime;
use crate::schema::{
AssetKind, EventPayload, IconRole, LoopMode, PROTOCOL_VERSION, SCHEMA_HASH, Theme,
};
use std::cell::RefCell;
use std::ffi::c_int;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::task::{Context, 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 compose_rust_renderer_run() -> c_int;
fn compose_rust_renderer_request_frame();
}
#[cfg(all(
renderer_host_exports,
renderer_linked,
not(any(test, feature = "mock-renderer"))
))]
unsafe extern "C" {
static compose_rust_renderer_host_exports: u8;
}
#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
extern "C" fn native_run() -> c_int {
#[cfg(renderer_host_exports)]
let _ = unsafe { std::ptr::read_volatile(&raw const compose_rust_renderer_host_exports) };
unsafe { compose_rust_renderer_run() }
}
#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
extern "C" fn native_request_frame() {
unsafe { compose_rust_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!(
"compose-rust: 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);
}
}
}
thread_local! {
static HOST_CALL_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
pub(crate) fn in_host_call() -> bool {
HOST_CALL_DEPTH.with(std::cell::Cell::get) > 0
}
struct HostCallGuard;
impl HostCallGuard {
fn enter() -> Self {
HOST_CALL_DEPTH.with(|depth| depth.set(depth.get() + 1));
Self
}
}
impl Drop for HostCallGuard {
fn drop(&mut self) {
HOST_CALL_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
}
}
pub(crate) fn request_frame_for_notifications() {
if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire) {
(renderer_api().request_frame)();
return;
}
request_frame_from_worker();
}
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 type RuntimeFactory = Arc<dyn Fn() -> Box<dyn Runtime> + Send + Sync>;
pub struct Host {
factory: RuntimeFactory,
theme: Theme,
window: crate::schema::Window,
runtime: Box<dyn Runtime>,
frame_waker: Waker,
pending_appends: Vec<PendingAppend>,
}
impl Host {
pub fn new(factory: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static) -> Self {
Self::from_factory(Arc::new(factory), launched_theme())
}
pub fn with_theme(
factory: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static,
theme: Theme,
) -> Self {
Self::from_factory(Arc::new(factory), theme)
}
fn from_factory(factory: RuntimeFactory, theme: Theme) -> Self {
crate::window::reset_window_size();
crate::message::reset_messages();
crate::asset::requeue_all();
crate::theme::install(theme);
let runtime = factory();
Self {
factory,
theme,
window: launched_window(),
runtime,
frame_waker: Waker::from(Arc::new(FrameWake)),
pending_appends: Vec::new(),
}
}
fn batch(&mut self) -> &mut crate::runtime::Batch {
self.runtime.batch_mut()
}
pub fn rebuild(&mut self) -> Result<&[u8], ProtocolError> {
let _call = HostCallGuard::enter();
self.batch().begin();
let (theme, window) = (self.theme, self.window);
self.batch().set_theme(theme);
self.batch().set_window(window);
self.runtime.rebuild();
self.flush_messages();
self.arm_scheduler_wake();
self.runtime.batch_mut().finish()
}
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.runtime.batch().arena()
}
pub fn resync(&mut self) -> Result<(&[u8], i64), ProtocolError> {
*self = Self::from_factory(self.factory.clone(), 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.batch().begin();
Ok((self.runtime.batch_mut().finish()?, 0))
}
fn render_and_finish(&mut self, result: i64) -> Result<(&[u8], i64), ProtocolError> {
self.batch().begin();
self.runtime.render();
self.flush_messages();
self.arm_scheduler_wake();
Ok((self.runtime.batch_mut().finish()?, result))
}
fn empty_batch(&mut self) -> Result<(&[u8], i64), ProtocolError> {
self.batch().begin();
Ok((self.runtime.batch_mut().finish()?, 0))
}
fn notification_activated(
&mut self,
action: u32,
key: &str,
) -> Result<(&[u8], i64), ProtocolError> {
let woke = {
let _dispatch_guard = EventDispatchGuard::enter();
crate::notification::activate(crate::notification::NotificationActivation {
key: key.to_owned(),
action,
})
};
self.batch().begin();
if woke {
self.runtime.render();
}
self.flush_messages();
self.arm_scheduler_wake();
Ok((self.runtime.batch_mut().finish()?, 0))
}
fn publish_notification_permission(
&mut self,
state: crate::schema::NotificationPermission,
) -> Result<(&[u8], i64), ProtocolError> {
self.batch().begin();
if crate::notification::publish_permission(state) {
self.runtime.render();
}
self.flush_messages();
self.arm_scheduler_wake();
Ok((self.runtime.batch_mut().finish()?, 0))
}
fn publish_design_system(
&mut self,
system: crate::schema::DesignSystem,
) -> Result<(&[u8], i64), ProtocolError> {
if crate::design::publish(system) {
return self.render_and_finish(0);
}
self.empty_batch()
}
pub fn dispatch(&mut self, event: HostEvent<'_>) -> Result<(&[u8], i64), ProtocolError> {
let _call = HostCallGuard::enter();
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),
EventPayload::NotificationActivated { action, key } => {
if event.node_id != 0 || event.handler_id != 0 {
return Err(ProtocolError::InvalidValueKind(0));
}
return self.notification_activated(action, key);
}
EventPayload::NotificationPermissionChanged(state) => {
if event.node_id != 0 || event.handler_id != 0 {
return Err(ProtocolError::InvalidValueKind(0));
}
return self.publish_notification_permission(state);
}
_ => {}
}
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
.runtime
.size_token(event.node_id)
.is_some_and(|token| crate::window::publish_node(token, size));
if !woke {
return self.empty_batch();
}
return self.render_and_finish(0);
}
crate::window::publish(size);
return self.render_and_finish(0);
}
let message_action = match event.payload {
EventPayload::Clicked => crate::message::take_action(event.handler_id),
_ => None,
};
if let Some(mut action) = message_action {
if event.node_id != 0 {
return Err(ProtocolError::InvalidValueKind(0));
}
let _dispatch_guard = EventDispatchGuard::enter();
self.runtime.run_in_context(&mut *action);
return self.render_and_finish(0);
}
if let EventPayload::ProtocolError { .. } = event.payload {
return Err(ProtocolError::InvalidValueKind(0));
}
let _dispatch_guard = EventDispatchGuard::enter();
let result = self.runtime.handle_event(&event)?;
self.render_and_finish(result)
}
pub fn render_frame(&mut self, _frame_time_nanos: u64) -> Result<&[u8], ProtocolError> {
let _call = HostCallGuard::enter();
if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire) {
self.batch().begin();
self.flush_notifications();
return self.runtime.batch_mut().finish();
}
FRAME_REQUESTED.store(false, Ordering::Release);
EVENT_DISPATCH_ACTIVE.store(false, Ordering::Release);
DEFERRED_FRAME_REQUEST.store(false, Ordering::Release);
self.batch().begin();
self.runtime.render();
self.flush_pending_appends();
self.flush_messages();
self.arm_scheduler_wake();
self.runtime.batch_mut().finish()
}
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) {
if let Some(theme) = crate::theme::take_pending() {
self.theme = theme;
self.batch().set_theme(theme);
}
let batch = self.runtime.batch_mut();
crate::asset::drain(|pending| {
batch.register_asset(pending.asset_id, pending.kind, pending.bytes);
});
crate::message::drain(|message| {
batch.show_message(
message.handler_id,
&message.text,
&message.action,
message.duration,
);
});
self.flush_notifications();
}
fn flush_notifications(&mut self) {
let batch = self.runtime.batch_mut();
let posts = crate::notification::drain(|command| batch.notification(command));
crate::notification::note_posted(posts);
}
fn flush_pending_appends(&mut self) {
let batch = self.runtime.batch_mut();
for pending in &self.pending_appends {
if pending.dirty {
batch.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> {
let batch = self.runtime.batch_mut();
batch.begin();
batch.register_asset(asset_id, kind, bytes);
batch.finish()
}
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> {
let batch = self.runtime.batch_mut();
batch.begin();
batch.release_asset(asset_id);
batch.finish()
}
pub fn set_text(
&mut self,
node_id: u32,
text: &str,
selection: Option<Selection>,
) -> Result<&[u8], ProtocolError> {
let batch = self.runtime.batch_mut();
batch.begin();
batch.set_text_node(node_id, text, selection);
batch.finish()
}
fn arm_scheduler_wake(&mut self) {
let mut context = Context::from_waker(&self.frame_waker);
if self.runtime.poll_work(&mut context).is_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<RuntimeFactory>> = 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<RuntimeFactory> {
APP.lock().map_or(None, |app| (*app).clone())
}
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_runtime(self, runtime: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static) {
let status = self.try_launch_runtime(runtime);
if status != STATUS_OK {
std::process::exit(EXIT_FAILURE);
}
}
pub fn try_launch_runtime(
self,
runtime: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static,
) -> i32 {
let factory: RuntimeFactory = Arc::new(runtime);
if let Ok(mut slot) = APP.lock() {
*slot = Some(factory);
}
if let Ok(mut slot) = THEME.lock() {
*slot = self.theme;
}
#[cfg(debug_assertions)]
for line in palette_report(&self.theme) {
eprintln!("{line}");
}
if let Ok(mut slot) = WINDOW.lock() {
*slot = self.window;
}
if self.mode != LoopMode::Renderer {
return STATUS_OK;
}
(renderer_api().run)()
}
}
pub fn launch_runtime(runtime: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static) {
LaunchBuilder::new().launch_runtime(runtime);
}
pub fn palette_report(theme: &Theme) -> Vec<String> {
let Some(palette) = theme.palette else {
return Vec::new();
};
let systems: Vec<crate::schema::DesignSystem> = if theme.adaptive {
crate::schema::DESIGN_SYSTEM_SCHEMA
.iter()
.filter_map(|variant| crate::schema::DesignSystem::try_from(variant.tag).ok())
.collect()
} else {
vec![theme.design_system]
};
systems
.into_iter()
.flat_map(|system| {
palette.check(system).into_iter().map(move |violation| {
format!("compose-rust: palette under {system:?}: {violation}")
})
})
.collect()
}
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!(
"compose-rust: 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 compose_rust_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 factory = 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::from_factory(factory, launched_theme());
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 compose_rust_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 compose_rust_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 compose_rust_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 compose_rust_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::*;
static LAUNCH: Mutex<()> = Mutex::new(());
fn launch_guard() -> std::sync::MutexGuard<'static, ()> {
LAUNCH
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[test]
fn malformed_ffi_input_returns_protocol_error() {
let mut output = MutationBatch::default();
let status = unsafe { compose_rust_host_dispatch_event(std::ptr::null(), 4, &mut output) };
assert_eq!(status, STATUS_PROTOCOL_ERROR);
}
#[test]
fn all_exports_isolate_invalid_calls() {
let _launch = launch_guard();
let mut output = MutationBatch::default();
unsafe {
assert_eq!(
compose_rust_host_init(std::ptr::null(), 1, &mut output),
STATUS_PROTOCOL_ERROR
);
assert_eq!(
compose_rust_host_render_frame(0, &mut output),
STATUS_NOT_INITIALIZED
);
compose_rust_host_release_batch(std::ptr::null_mut());
}
compose_rust_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([
compose_rust_host_init as *const (),
compose_rust_host_dispatch_event as *const (),
compose_rust_host_render_frame as *const (),
compose_rust_host_release_batch as *const (),
compose_rust_host_shutdown as *const (),
]);
#[cfg(all(target_os = "windows", target_env = "msvc"))]
const EXPORT_DIRECTIVES: &str = concat!(
" /EXPORT:compose_rust_host_init",
" /EXPORT:compose_rust_host_dispatch_event",
" /EXPORT:compose_rust_host_render_frame",
" /EXPORT:compose_rust_host_release_batch",
" /EXPORT:compose_rust_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 {}