use std::cell::RefCell;
use std::rc::Rc;
use std::time::Instant;
use geometry_core::Rect;
use layout_core::AvailableSpace;
use platform_core::{Event, WindowCommand};
use renderer_core::{BorderRadius, Color, DrawCommand};
use ui_core::{ComponentList, EventResult, NodeId, Surface, compute_layout, mark_dirty};
use ui_tree::{Component, NodeVec, RenderNode};
use crate::app_context::AppCtx;
pub type DrawList = Vec<DrawCommand>;
pub type WindowCommands = Vec<WindowCommand>;
pub use platform_core::Event as PluginEvent;
pub use renderer_core::Color as PluginColor;
pub fn composite(rect: Rect, image_salt: u64, mut commands: DrawList) -> RenderNode {
if image_salt != 0 {
const ID_BITS: u32 = 40;
for cmd in &mut commands {
if let DrawCommand::Image { data, .. } = cmd {
let salted = (image_salt << ID_BITS) | (data.id & ((1u64 << ID_BITS) - 1));
std::sync::Arc::make_mut(data).id = salted;
}
}
}
RenderNode::Clip {
rect,
radius: BorderRadius::zero(),
children: NodeVec::collect([RenderNode::Transform {
matrix: [1.0, 0.0, 0.0, 1.0, rect.x, rect.y],
children: NodeVec::collect(commands.into_iter().map(RenderNode::Primitive)),
}]),
}
}
pub trait EmbeddedApp: 'static {
fn build(&mut self);
fn layout_root(&self) -> NodeId;
fn view(&self) -> RenderNode;
fn on_event(&mut self, event: &Event) -> EventResult;
fn relayout_viewports(&mut self) {}
fn activate(&mut self) {}
fn on_frame(&mut self, _ctx: &mut AppCtx) {}
fn clear_color(&self) -> Option<Color> {
None
}
fn title(&self) -> String;
fn icon(&self) -> Option<Vec<u8>> {
None
}
fn id(&self) -> String;
}
struct EmbeddedComponent(Rc<RefCell<Box<dyn EmbeddedApp>>>);
impl Component for EmbeddedComponent {
fn view(&self) -> RenderNode {
self.0.borrow().view()
}
fn on_event(&mut self, event: &Event) -> EventResult {
self.0.borrow_mut().on_event(event)
}
fn debug_name(&self) -> &'static str {
"PluginRoot"
}
}
pub struct PluginInstance {
embedded: Rc<RefCell<Box<dyn EmbeddedApp>>>,
tree: ComponentList,
root: NodeId,
size: (f32, f32),
task_waker_installed: bool,
surface: Rc<Surface>,
}
impl PluginInstance {
pub fn new(embedded: Box<dyn EmbeddedApp>) -> Self {
let surface = Surface::new();
let embedded = Rc::new(RefCell::new(embedded));
let (root, tree) = {
let _g = surface.enter();
embedded.borrow_mut().build();
let root = embedded.borrow().layout_root();
let tree = ComponentList::new(EmbeddedComponent(Rc::clone(&embedded)));
(root, tree)
};
Self {
surface,
embedded,
tree,
root,
size: (0.0, 0.0),
task_waker_installed: false,
}
}
pub fn relayout(&mut self, width: f32, height: f32) {
let _g = self.surface.enter();
self.size = (width, height);
let _ = mark_dirty(self.root);
let _ = compute_layout(
self.root,
AvailableSpace::Definite(width),
AvailableSpace::Definite(height),
);
let embedded = &self.embedded;
reactive_core::batch(|| embedded.borrow_mut().relayout_viewports());
self.tree.bump_force_ticks();
}
pub fn relayout_dirty(&self) {
let _g = self.surface.enter();
ui_core::relayout_if_dirty();
}
pub fn paint(&self) -> DrawList {
let _g = self.surface.enter();
self.tree.commands().clone()
}
pub fn generation(&self) -> u64 {
let _g = self.surface.enter();
self.tree.generation()
}
pub fn motion_active(&self) -> bool {
let _g = self.surface.enter();
motion_core::has_active()
}
pub fn on_event(&mut self, event: &Event) -> bool {
let _g = self.surface.enter();
ui_core::observe_keyboard(event);
ui_core::observe_pointer(event);
self.tree.on_event(event) == EventResult::Handled
}
pub fn dispatch_overlays(&self, event: &Event) -> bool {
let _g = self.surface.enter();
ui_core::observe_keyboard(event);
ui_core::observe_pointer(event);
reactive_core::batch(|| ui_core::dispatch_overlays(event) == EventResult::Handled)
}
pub fn end_frame(&self) {
let _g = self.surface.enter();
ui_core::end_keyboard_frame();
}
pub fn motion_tick(&self, now: Instant) {
let _g = self.surface.enter();
reactive_core::begin_batch();
motion_core::tick(now);
reactive_core::end_batch();
}
pub fn drain_window_commands(&self) -> WindowCommands {
let _g = self.surface.enter();
platform_core::take_window_commands()
}
pub fn set_system_dark(&self, dark: bool) {
let _g = self.surface.enter();
reactive_core::begin_batch();
theme_core::set_system_dark(dark);
reactive_core::end_batch();
}
pub fn on_frame(&mut self, ctx: &mut AppCtx) {
let _g = self.surface.enter();
if !self.task_waker_installed {
if let Some(waker) = ctx.redraw_waker() {
reactive_core::set_task_waker(move || waker.wake());
self.task_waker_installed = true;
}
}
reactive_core::drain_tasks();
let embedded = &self.embedded;
reactive_core::batch(|| embedded.borrow_mut().on_frame(ctx));
}
pub fn activate(&mut self) {
let _g = self.surface.enter();
let embedded = &self.embedded;
reactive_core::batch(|| embedded.borrow_mut().activate());
self.tree.bump_force_ticks();
}
pub fn clear_color(&self) -> Option<Color> {
let _g = self.surface.enter();
self.embedded.borrow().clear_color()
}
pub fn title(&self) -> String {
let _g = self.surface.enter();
self.embedded.borrow().title()
}
pub fn icon(&self) -> Option<Vec<u8>> {
let _g = self.surface.enter();
self.embedded.borrow().icon()
}
pub fn id(&self) -> String {
let _g = self.surface.enter();
self.embedded.borrow().id()
}
}
impl Drop for PluginInstance {
fn drop(&mut self) {
reactive_core::cancel_tasks_for(self.surface.handle());
}
}
#[doc(hidden)]
pub fn __plugin_create(embedded: Box<dyn EmbeddedApp>) -> *mut PluginInstance {
Box::into_raw(Box::new(PluginInstance::new(embedded)))
}
#[doc(hidden)]
pub unsafe fn __plugin_destroy(inst: *mut PluginInstance) {
drop(unsafe { Box::from_raw(inst) });
}
macro_rules! plugin_shim {
($(#[$m:meta])* $vis_fn:ident ($($arg:ident : $ty:ty),*) $(-> $ret:ty)? => $method:ident) => {
$(#[$m])*
#[doc(hidden)]
pub unsafe fn $vis_fn(inst: *mut PluginInstance $(, $arg: $ty)*) $(-> $ret)? {
unsafe { (*inst).$method($($arg),*) }
}
};
}
plugin_shim!(__plugin_relayout(width: f32, height: f32) => relayout);
plugin_shim!(__plugin_relayout_dirty() => relayout_dirty);
plugin_shim!(__plugin_paint() -> DrawList => paint);
plugin_shim!(__plugin_generation() -> u64 => generation);
plugin_shim!(__plugin_on_event(event: &Event) -> bool => on_event);
plugin_shim!(__plugin_dispatch_overlays(event: &Event) -> bool => dispatch_overlays);
plugin_shim!(__plugin_end_frame() => end_frame);
plugin_shim!(__plugin_motion_tick(now: Instant) => motion_tick);
plugin_shim!(__plugin_motion_active() -> bool => motion_active);
plugin_shim!(__plugin_drain_window_commands() -> WindowCommands => drain_window_commands);
plugin_shim!(__plugin_set_system_dark(dark: bool) => set_system_dark);
plugin_shim!(__plugin_activate() => activate);
plugin_shim!(__plugin_clear_color() -> Option<Color> => clear_color);
plugin_shim!(__plugin_title() -> String => title);
plugin_shim!(__plugin_icon() -> Option<Vec<u8>> => icon);
plugin_shim!(__plugin_id() -> String => id);
#[doc(hidden)]
pub unsafe fn __plugin_on_frame(inst: *mut PluginInstance, ctx: &mut AppCtx) {
unsafe { (*inst).on_frame(ctx) }
}
pub const TELAR_PLUGIN_ABI: u32 = 1;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct PluginVTable {
pub abi: u32,
pub create: unsafe extern "Rust" fn(&[String]) -> *mut PluginInstance,
pub destroy: unsafe extern "Rust" fn(*mut PluginInstance),
pub relayout: unsafe extern "Rust" fn(*mut PluginInstance, f32, f32),
pub relayout_dirty: unsafe extern "Rust" fn(*mut PluginInstance),
pub paint: unsafe extern "Rust" fn(*mut PluginInstance) -> DrawList,
pub generation: unsafe extern "Rust" fn(*mut PluginInstance) -> u64,
pub on_event: unsafe extern "Rust" fn(*mut PluginInstance, &Event) -> bool,
pub dispatch_overlays: unsafe extern "Rust" fn(*mut PluginInstance, &Event) -> bool,
pub end_frame: unsafe extern "Rust" fn(*mut PluginInstance),
pub motion_tick: unsafe extern "Rust" fn(*mut PluginInstance, Instant),
pub motion_active: unsafe extern "Rust" fn(*mut PluginInstance) -> bool,
pub drain_window_commands: unsafe extern "Rust" fn(*mut PluginInstance) -> WindowCommands,
pub set_system_dark: unsafe extern "Rust" fn(*mut PluginInstance, bool),
pub activate: unsafe extern "Rust" fn(*mut PluginInstance),
pub clear_color: unsafe extern "Rust" fn(*mut PluginInstance) -> Option<Color>,
pub title: unsafe extern "Rust" fn(*mut PluginInstance) -> String,
pub icon: unsafe extern "Rust" fn(*mut PluginInstance) -> Option<Vec<u8>>,
pub id: unsafe extern "Rust" fn(*mut PluginInstance) -> String,
pub on_frame: unsafe extern "Rust" fn(*mut PluginInstance, &mut AppCtx),
}
#[macro_export]
macro_rules! plugin {
($factory:expr) => {
#[unsafe(no_mangle)]
pub static _rsx_plugin_vtable: $crate::plugin::PluginVTable = {
unsafe extern "Rust" fn create(
args: &[::std::string::String],
) -> *mut $crate::plugin::PluginInstance {
$crate::plugin::__plugin_create(($factory)(args))
}
$crate::plugin::PluginVTable {
abi: $crate::plugin::TELAR_PLUGIN_ABI,
create,
destroy: $crate::plugin::__plugin_destroy,
relayout: $crate::plugin::__plugin_relayout,
relayout_dirty: $crate::plugin::__plugin_relayout_dirty,
paint: $crate::plugin::__plugin_paint,
generation: $crate::plugin::__plugin_generation,
on_event: $crate::plugin::__plugin_on_event,
dispatch_overlays: $crate::plugin::__plugin_dispatch_overlays,
end_frame: $crate::plugin::__plugin_end_frame,
motion_tick: $crate::plugin::__plugin_motion_tick,
motion_active: $crate::plugin::__plugin_motion_active,
drain_window_commands: $crate::plugin::__plugin_drain_window_commands,
set_system_dark: $crate::plugin::__plugin_set_system_dark,
activate: $crate::plugin::__plugin_activate,
clear_color: $crate::plugin::__plugin_clear_color,
title: $crate::plugin::__plugin_title,
icon: $crate::plugin::__plugin_icon,
id: $crate::plugin::__plugin_id,
on_frame: $crate::plugin::__plugin_on_frame,
}
};
};
}
#[cfg(feature = "plugin-host")]
pub use host::{LoadedPlugin, load_plugin};
#[cfg(feature = "plugin-host")]
mod host {
use super::*;
use std::path::Path;
pub struct LoadedPlugin {
inst: *mut PluginInstance,
vtable: PluginVTable,
_lib: libloading::Library,
}
pub fn load_plugin(
path: &Path,
args: &[String],
) -> Result<LoadedPlugin, Box<dyn std::error::Error>> {
let lib = crate::dylib::open(path)?;
let symbol: libloading::Symbol<*const PluginVTable> =
unsafe { lib.get(b"_rsx_plugin_vtable\0")? };
let ptr: *const PluginVTable = *symbol;
let abi = unsafe { *ptr.cast::<u32>() };
if abi != TELAR_PLUGIN_ABI {
return Err(format!(
"plugin built for ABI {abi}, host is ABI {TELAR_PLUGIN_ABI} — rebuild {}",
path.display()
)
.into());
}
let vtable = unsafe { *ptr };
let inst = unsafe { (vtable.create)(args) };
if inst.is_null() {
return Err("plugin create returned null".into());
}
Ok(LoadedPlugin {
inst,
vtable,
_lib: lib,
})
}
impl LoadedPlugin {
pub fn relayout(&self, width: f32, height: f32) {
unsafe { (self.vtable.relayout)(self.inst, width, height) }
}
pub fn relayout_dirty(&self) {
unsafe { (self.vtable.relayout_dirty)(self.inst) }
}
pub fn paint(&self) -> DrawList {
unsafe { (self.vtable.paint)(self.inst) }
}
pub fn generation(&self) -> u64 {
unsafe { (self.vtable.generation)(self.inst) }
}
pub fn on_event(&self, event: &Event) -> bool {
unsafe { (self.vtable.on_event)(self.inst, event) }
}
pub fn dispatch_overlays(&self, event: &Event) -> bool {
unsafe { (self.vtable.dispatch_overlays)(self.inst, event) }
}
pub fn end_frame(&self) {
unsafe { (self.vtable.end_frame)(self.inst) }
}
pub fn motion_tick(&self, now: Instant) {
unsafe { (self.vtable.motion_tick)(self.inst, now) }
}
pub fn motion_active(&self) -> bool {
unsafe { (self.vtable.motion_active)(self.inst) }
}
pub fn drain_window_commands(&self) -> WindowCommands {
unsafe { (self.vtable.drain_window_commands)(self.inst) }
}
pub fn set_system_dark(&self, dark: bool) {
unsafe { (self.vtable.set_system_dark)(self.inst, dark) }
}
pub fn activate(&self) {
unsafe { (self.vtable.activate)(self.inst) }
}
pub fn clear_color(&self) -> Option<Color> {
unsafe { (self.vtable.clear_color)(self.inst) }
}
pub fn title(&self) -> String {
unsafe { (self.vtable.title)(self.inst) }
}
pub fn icon(&self) -> Option<Vec<u8>> {
unsafe { (self.vtable.icon)(self.inst) }
}
pub fn id(&self) -> String {
unsafe { (self.vtable.id)(self.inst) }
}
pub fn on_frame(&self, ctx: &mut AppCtx) {
unsafe { (self.vtable.on_frame)(self.inst, ctx) }
}
}
impl Drop for LoadedPlugin {
fn drop(&mut self) {
unsafe { (self.vtable.destroy)(self.inst) }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use layout_core::LayoutStyle;
use platform_core::{Key, ModifiersState};
use renderer_core::RectStyle;
struct Stub {
node: Option<NodeId>,
seen: usize,
}
impl Stub {
fn new() -> Self {
Self {
node: None,
seen: 0,
}
}
}
impl EmbeddedApp for Stub {
fn build(&mut self) {
let (node, _) =
ui_core::new_leaf(LayoutStyle::new().width(40.0).height(20.0)).expect("leaf");
self.node = Some(node);
}
fn layout_root(&self) -> NodeId {
self.node.expect("build ran first")
}
fn view(&self) -> RenderNode {
RenderNode::rect(Rect::new(0.0, 0.0, 40.0, 20.0), RectStyle::default())
}
fn on_event(&mut self, _event: &Event) -> EventResult {
self.seen += 1;
EventResult::Ignored
}
fn title(&self) -> String {
"stub".into()
}
fn id(&self) -> String {
"stub".into()
}
}
fn shift() -> ModifiersState {
ModifiersState {
is_shift: true,
..ModifiersState::default()
}
}
crate::plugin!(|_args: &[String]| -> Box<dyn EmbeddedApp> { Box::new(Stub::new()) });
#[test]
fn the_export_macro_builds_a_vtable_at_the_current_abi() {
assert_eq!(_rsx_plugin_vtable.abi, TELAR_PLUGIN_ABI);
let inst = unsafe { (_rsx_plugin_vtable.create)(&[]) };
assert!(!inst.is_null());
assert_eq!(unsafe { (_rsx_plugin_vtable.id)(inst) }, "stub");
unsafe { (_rsx_plugin_vtable.destroy)(inst) };
}
#[test]
fn a_plugin_records_the_modifiers_it_is_handed() {
let mut inst = PluginInstance::new(Box::new(Stub::new()));
let _g = inst.surface.enter();
assert_eq!(ui_core::modifiers(), ModifiersState::default());
drop(_g);
inst.on_event(&Event::ModifiersChanged { modifiers: shift() });
let _g = inst.surface.enter();
assert!(
ui_core::modifiers().is_shift,
"a shift-drag inside a plugin is indistinguishable from a plain one without this"
);
}
#[test]
fn an_overlay_event_reaches_the_registry_too() {
let inst = PluginInstance::new(Box::new(Stub::new()));
inst.dispatch_overlays(&Event::ModifiersChanged { modifiers: shift() });
let _g = inst.surface.enter();
assert!(ui_core::modifiers().is_shift);
}
#[test]
fn a_press_answers_for_one_frame_and_end_frame_closes_it() {
let mut inst = PluginInstance::new(Box::new(Stub::new()));
inst.on_event(&Event::KeyPressed {
key: Key::Char('c'),
modifiers: ModifiersState::default(),
});
{
let _g = inst.surface.enter();
assert!(ui_core::key_pressed(&Key::Char('c')));
}
inst.end_frame();
let _g = inst.surface.enter();
assert!(
!ui_core::key_pressed(&Key::Char('c')),
"without end_frame the press answers forever, not for its frame"
);
assert!(
ui_core::key_held(&Key::Char('c')),
"held is not what end_frame clears"
);
}
#[test]
fn a_plugin_paints_and_its_generation_is_stable_between_frames() {
let mut inst = PluginInstance::new(Box::new(Stub::new()));
inst.relayout(40.0, 20.0);
assert!(!inst.paint().is_empty());
assert_eq!(inst.generation(), inst.generation());
}
#[test]
fn the_driver_forwards_metadata_from_the_embedded_app() {
let inst = PluginInstance::new(Box::new(Stub::new()));
assert_eq!(inst.title(), "stub");
assert_eq!(inst.id(), "stub");
assert_eq!(inst.clear_color(), None);
}
#[test]
fn composite_translates_into_the_sub_rect_and_clips_to_it() {
let rect = Rect::new(10.0, 20.0, 100.0, 50.0);
let node = composite(
rect,
0,
vec![DrawCommand::Rect {
rect: Rect::new(0.0, 0.0, 5.0, 5.0),
style: std::sync::Arc::new(RectStyle::default()),
}],
);
match node {
RenderNode::Clip {
rect: clip,
children,
..
} => {
assert_eq!(clip, rect, "clipped to the host's sub-rect");
assert!(!children.is_empty());
}
_ => panic!("expected the plugin's frame to be wrapped in a clip"),
}
}
}