#![cfg(target_os = "ios")]
use std::any::type_name;
use std::cell::RefCell;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use objc2::msg_send;
use objc2::runtime::{AnyClass, AnyObject, ClassBuilder, Sel};
use objc2::sel;
use objc2_foundation::{NSPoint, NSRect, NSSize};
use truce_core::editor::{Editor, PluginContext, RawWindowHandle};
use truce_gui_types::interaction::{self, InputEvent, InteractionState, MouseButton, ParamEdit};
use truce_gui_types::ios::{TouchPhase, fnv1a_64, ivar_offset};
use truce_gui_types::layout::{GridLayout, Layout, PluginLayout};
use truce_gui_types::theme::Theme;
use truce_params::Params;
use crate::backend_cpu::CpuBackend;
use crate::platform::EditorScale;
use crate::render_core::{build_snapshot_closures, render_widgets};
pub struct BuiltinEditor<P: Params + 'static> {
params: Arc<P>,
layout: Layout,
theme: Theme,
backend: Option<CpuBackend>,
interaction: InteractionState,
context: Option<PluginContext>,
inner: Arc<RefCell<Option<Inner<P>>>>,
needs_repaint: Arc<AtomicBool>,
scale: EditorScale,
}
unsafe impl<P: Params + 'static> Send for BuiltinEditor<P> {}
struct Inner<P: Params + 'static> {
child_view: *mut AnyObject,
display_link: *mut AnyObject,
tick_target: *mut AnyObject,
logical_w: u32,
logical_h: u32,
last_painted_values: Vec<f32>,
params: Arc<P>,
layout: Layout,
theme: Theme,
backend: Option<CpuBackend>,
interaction: InteractionState,
context: Option<PluginContext>,
needs_repaint: Arc<AtomicBool>,
scale: EditorScale,
}
impl<P: Params + 'static> BuiltinEditor<P> {
#[must_use]
pub fn new(params: Arc<P>, layout: PluginLayout) -> Self {
Self::new_with(params, Layout::Rows(layout))
}
#[must_use]
pub fn new_grid(params: Arc<P>, layout: GridLayout) -> Self {
Self::new_with(params, Layout::Grid(layout))
}
fn new_with(params: Arc<P>, layout: Layout) -> Self {
Self {
params,
layout,
theme: Theme::dark(),
backend: None,
interaction: InteractionState::default(),
context: None,
inner: Arc::new(RefCell::new(None)),
needs_repaint: Arc::new(AtomicBool::new(true)),
scale: EditorScale::new(crate::platform::main_screen_scale()),
}
}
}
impl<P: Params + 'static> Editor for BuiltinEditor<P> {
fn size(&self) -> (u32, u32) {
match &self.layout {
Layout::Rows(p) => (p.width, p.height),
Layout::Grid(g) => (g.width, g.height),
}
}
fn open(&mut self, parent: RawWindowHandle, context: PluginContext) {
let RawWindowHandle::UiKit(parent_ptr) = parent else {
log::warn!("iOS BuiltinEditor::open got non-UiKit parent handle");
return;
};
if parent_ptr.is_null() {
log::warn!("iOS BuiltinEditor::open got null parent pointer");
return;
}
let (lw, lh) = self.size();
self.context = Some(context.clone());
let inner = Inner {
child_view: std::ptr::null_mut(),
display_link: std::ptr::null_mut(),
tick_target: std::ptr::null_mut(),
logical_w: lw,
logical_h: lh,
last_painted_values: Vec::new(),
params: Arc::clone(&self.params),
layout: self.layout.clone(),
theme: self.theme.clone(),
backend: None,
interaction: std::mem::take(&mut self.interaction),
context: Some(context),
needs_repaint: Arc::clone(&self.needs_repaint),
scale: self.scale.clone(),
};
let slot = Arc::clone(&self.inner);
*slot.borrow_mut() = Some(inner);
let (view, link) =
unsafe { install_editor_view::<P>(parent_ptr.cast(), lw, lh, Arc::clone(&slot)) };
if view.is_null() {
log::warn!("iOS BuiltinEditor::open: install_editor_view returned null");
return;
}
if let Some(inner_mut) = slot.borrow_mut().as_mut() {
inner_mut.child_view = view;
inner_mut.display_link = link;
inner_mut.tick_target = view; }
}
fn close(&mut self) {
let Some(inner) = self.inner.borrow_mut().take() else {
return;
};
unsafe {
if !inner.display_link.is_null() {
let _: () = msg_send![inner.display_link, invalidate];
let _: () = msg_send![inner.display_link, release];
}
if !inner.child_view.is_null() {
let cls: &AnyClass = msg_send![inner.child_view, class];
let base: *const u8 = inner.child_view.cast();
let ivar_ptr: *const *mut std::ffi::c_void =
base.add(ivar_offset(cls, INNER_PTR_IVAR)).cast();
let leaked = *ivar_ptr as *const RefCell<Option<Inner<P>>>;
if !leaked.is_null() {
let _ = Arc::from_raw(leaked); }
let _: () = msg_send![inner.child_view, removeFromSuperview];
}
}
self.interaction = inner.interaction;
self.backend = inner.backend;
self.context = None;
}
}
const INNER_PTR_IVAR: &std::ffi::CStr = c"_truce_inner_ptr";
unsafe extern "C" {
static NSRunLoopCommonModes: *const AnyObject;
}
unsafe fn install_editor_view<P: Params + 'static>(
parent: *mut AnyObject,
logical_w: u32,
logical_h: u32,
inner: Arc<RefCell<Option<Inner<P>>>>,
) -> (*mut AnyObject, *mut AnyObject) {
unsafe {
let class_name_owned = format!(
"TruceiOSEditorView_{:x}",
fnv1a_64(type_name::<Inner<P>>().as_bytes())
);
let class_name = std::ffi::CString::new(class_name_owned).expect("ascii class name");
let uiview = AnyClass::get(c"UIView").expect("UIView missing");
let cls: &AnyClass = if let Some(existing) = AnyClass::get(class_name.as_c_str()) {
existing
} else {
{
let mut builder = ClassBuilder::new(class_name.as_c_str(), uiview)
.expect("class name unique per type-monomorphization");
builder.add_ivar::<*mut std::ffi::c_void>(INNER_PTR_IVAR);
builder.add_method(
sel!(tick:),
tick_thunk::<P> as unsafe extern "C" fn(_, _, _),
);
builder.add_method(
sel!(touchesBegan:withEvent:),
touches_began::<P> as unsafe extern "C" fn(_, _, _, _),
);
builder.add_method(
sel!(touchesMoved:withEvent:),
touches_moved::<P> as unsafe extern "C" fn(_, _, _, _),
);
builder.add_method(
sel!(touchesEnded:withEvent:),
touches_ended::<P> as unsafe extern "C" fn(_, _, _, _),
);
builder.add_method(
sel!(touchesCancelled:withEvent:),
touches_cancelled::<P> as unsafe extern "C" fn(_, _, _, _),
);
builder.register()
}
};
let frame = NSRect {
origin: NSPoint { x: 0.0, y: 0.0 },
size: NSSize {
width: f64::from(logical_w),
height: f64::from(logical_h),
},
};
let alloc: *mut AnyObject = msg_send![cls, alloc];
let view: *mut AnyObject = msg_send![alloc, initWithFrame: frame];
if view.is_null() {
return (std::ptr::null_mut(), std::ptr::null_mut());
}
let color_cls = AnyClass::get(c"UIColor").expect("UIColor missing");
let bg: *mut AnyObject = msg_send![color_cls, darkGrayColor];
let _: () = msg_send![view, setBackgroundColor: bg];
let _: () = msg_send![view, setUserInteractionEnabled: true];
let _: () = msg_send![view, setMultipleTouchEnabled: true];
let leaked: *const RefCell<Option<Inner<P>>> = Arc::into_raw(inner);
let base = view.cast::<u8>();
let ivar_ptr: *mut *mut std::ffi::c_void =
base.add(ivar_offset(cls, INNER_PTR_IVAR)).cast();
*ivar_ptr = leaked as *mut std::ffi::c_void;
let _: () = msg_send![parent, addSubview: view];
let dl_cls = AnyClass::get(c"CADisplayLink").expect("CADisplayLink missing");
let link: *mut AnyObject =
msg_send![dl_cls, displayLinkWithTarget: view, selector: sel!(tick:)];
if link.is_null() {
return (view, std::ptr::null_mut());
}
let _: () = msg_send![link, retain];
let run_loop_cls = AnyClass::get(c"NSRunLoop").expect("NSRunLoop missing");
let main: *mut AnyObject = msg_send![run_loop_cls, mainRunLoop];
let mode: *const AnyObject = NSRunLoopCommonModes;
let _: () = msg_send![link, addToRunLoop: main, forMode: mode];
(view, link)
}
}
unsafe fn borrow_inner_arc<P: Params + 'static>(
self_: &AnyObject,
) -> Option<Arc<RefCell<Option<Inner<P>>>>> {
unsafe {
let cls: &AnyClass = msg_send![self_, class];
let base: *const u8 = std::ptr::from_ref::<AnyObject>(self_).cast();
let ivar_ptr: *const *mut std::ffi::c_void =
base.add(ivar_offset(cls, INNER_PTR_IVAR)).cast();
let leaked = (*ivar_ptr).cast_const().cast::<RefCell<Option<Inner<P>>>>();
if leaked.is_null() {
return None;
}
let arc = Arc::from_raw(leaked);
let cloned = Arc::clone(&arc);
let _ = Arc::into_raw(arc); Some(cloned)
}
}
unsafe extern "C" fn tick_thunk<P: Params + 'static>(
self_: &AnyObject,
_cmd: Sel,
_sender: *mut AnyObject,
) {
unsafe {
let Some(arc) = borrow_inner_arc::<P>(self_) else {
return;
};
let Ok(mut guard) = arc.try_borrow_mut() else {
return;
};
let Some(inner) = guard.as_mut() else { return };
tick(inner);
}
}
unsafe extern "C" fn touches_began<P: Params + 'static>(
self_: &AnyObject,
_cmd: Sel,
touches: *mut AnyObject,
_event: *mut AnyObject,
) {
unsafe {
dispatch_touch::<P>(self_, touches, TouchPhase::Began);
}
}
unsafe extern "C" fn touches_moved<P: Params + 'static>(
self_: &AnyObject,
_cmd: Sel,
touches: *mut AnyObject,
_event: *mut AnyObject,
) {
unsafe {
dispatch_touch::<P>(self_, touches, TouchPhase::Moved);
}
}
unsafe extern "C" fn touches_ended<P: Params + 'static>(
self_: &AnyObject,
_cmd: Sel,
touches: *mut AnyObject,
_event: *mut AnyObject,
) {
unsafe {
dispatch_touch::<P>(self_, touches, TouchPhase::Ended);
}
}
unsafe extern "C" fn touches_cancelled<P: Params + 'static>(
self_: &AnyObject,
_cmd: Sel,
touches: *mut AnyObject,
_event: *mut AnyObject,
) {
unsafe {
dispatch_touch::<P>(self_, touches, TouchPhase::Ended);
}
}
unsafe fn dispatch_touch<P: Params + 'static>(
self_: &AnyObject,
touches: *mut AnyObject,
phase: TouchPhase,
) {
unsafe {
let Some(arc) = borrow_inner_arc::<P>(self_) else {
return;
};
let Ok(mut guard) = arc.try_borrow_mut() else {
return;
};
let Some(inner) = guard.as_mut() else { return };
let view_ptr: *mut AnyObject = std::ptr::from_ref::<AnyObject>(self_).cast_mut();
let touch_count: usize = msg_send![touches, count];
let enumerator: *mut AnyObject = msg_send![touches, objectEnumerator];
let mut events: Vec<InputEvent> = Vec::with_capacity(touch_count);
events.extend(NSEnumerator(enumerator).map(|touch| {
let pt: NSPoint = msg_send![touch, locationInView: view_ptr];
#[allow(clippy::cast_possible_truncation)]
let (x, y) = (pt.x as f32, pt.y as f32);
let pointer_id = touch as u64;
match phase {
TouchPhase::Began => InputEvent::MouseDown {
pointer_id,
x,
y,
button: MouseButton::Left,
},
TouchPhase::Moved => InputEvent::MouseMove { pointer_id, x, y },
TouchPhase::Ended => InputEvent::MouseUp {
pointer_id,
x,
y,
button: MouseButton::Left,
},
}
}));
if events.is_empty() {
return;
}
let closures = build_snapshot_closures(&inner.params, inner.context.as_ref());
let snapshot = closures.as_snapshot();
let edits =
interaction::dispatch(&events, &inner.layout, &snapshot, &mut inner.interaction);
let context = inner.context.clone();
let params = Arc::clone(&inner.params);
let needs_repaint = Arc::clone(&inner.needs_repaint);
drop(guard);
drop(arc);
for edit in edits {
apply_edit(context.as_ref(), ¶ms, &needs_repaint, edit);
}
}
}
fn apply_edit<P: Params + 'static>(
context: Option<&PluginContext>,
params: &Arc<P>,
needs_repaint: &Arc<AtomicBool>,
edit: ParamEdit,
) {
match edit {
ParamEdit::Begin { id } => {
if let Some(ctx) = context {
ctx.begin_edit(id);
}
}
ParamEdit::Set { id, normalized } => {
params.set_normalized(id, f64::from(normalized));
if let Some(ctx) = context {
ctx.set_param(id, f64::from(normalized));
}
needs_repaint.store(true, Ordering::Release);
}
ParamEdit::End { id } => {
if let Some(ctx) = context {
ctx.end_edit(id);
}
}
}
}
fn tick<P: Params + 'static>(inner: &mut Inner<P>) {
let _ = inner.needs_repaint.swap(false, Ordering::AcqRel);
let (w, h) = (inner.logical_w, inner.logical_h);
let scale = inner.scale.get_f32();
let closures = build_snapshot_closures(&inner.params, inner.context.as_ref());
let snapshot = closures.as_snapshot();
let backend = inner.backend.get_or_insert_with(|| {
CpuBackend::new(w, h, scale).expect("CpuBackend allocation failed (out of memory?)")
});
match &inner.layout {
Layout::Rows(pl) => inner.interaction.build_regions(pl),
Layout::Grid(gl) => inner.interaction.build_regions_grid(gl),
}
render_widgets(
&inner.layout,
&inner.theme,
&mut inner.interaction,
&snapshot,
backend,
);
inner
.last_painted_values
.resize(inner.interaction.knob_regions.len(), 0.0);
for (slot, r) in inner
.last_painted_values
.iter_mut()
.zip(inner.interaction.knob_regions.iter())
{
*slot = r.normalized_value;
}
unsafe {
blit_pixmap_to_layer(
inner.child_view,
backend.width(),
backend.height(),
backend.data(),
);
}
}
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
fn CGDataProviderCreateWithData(
info: *mut std::ffi::c_void,
data: *const u8,
size: usize,
release_callback: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const u8, usize)>,
) -> *mut std::ffi::c_void;
fn CGDataProviderRelease(provider: *mut std::ffi::c_void);
fn CGColorSpaceCreateDeviceRGB() -> *mut std::ffi::c_void;
fn CGColorSpaceRelease(cs: *mut std::ffi::c_void);
fn CGImageCreate(
width: usize,
height: usize,
bits_per_component: usize,
bits_per_pixel: usize,
bytes_per_row: usize,
color_space: *mut std::ffi::c_void,
bitmap_info: u32,
provider: *mut std::ffi::c_void,
decode: *const f32,
should_interpolate: bool,
intent: i32,
) -> *mut std::ffi::c_void;
fn CGImageRelease(image: *mut std::ffi::c_void);
}
const K_CG_BITMAP_BYTE_ORDER_32_BIG: u32 = 4 << 12;
const K_CG_IMAGE_ALPHA_PREMULTIPLIED_LAST: u32 = 1;
const K_CG_RENDERING_INTENT_DEFAULT: i32 = 0;
unsafe fn blit_pixmap_to_layer(view: *mut AnyObject, width: u32, height: u32, rgba: &[u8]) {
unsafe {
let bytes_per_row = (width as usize) * 4;
let provider =
CGDataProviderCreateWithData(std::ptr::null_mut(), rgba.as_ptr(), rgba.len(), None);
if provider.is_null() {
return;
}
let cs = CGColorSpaceCreateDeviceRGB();
let info = K_CG_BITMAP_BYTE_ORDER_32_BIG | K_CG_IMAGE_ALPHA_PREMULTIPLIED_LAST;
let image = CGImageCreate(
width as usize,
height as usize,
8,
32,
bytes_per_row,
cs,
info,
provider,
std::ptr::null(),
false,
K_CG_RENDERING_INTENT_DEFAULT,
);
CGDataProviderRelease(provider);
CGColorSpaceRelease(cs);
if image.is_null() {
return;
}
let layer: *mut AnyObject = msg_send![view, layer];
let _: () = msg_send![layer, setContents: image];
CGImageRelease(image);
}
}
struct NSEnumerator(*mut AnyObject);
impl Iterator for NSEnumerator {
type Item = *mut AnyObject;
fn next(&mut self) -> Option<Self::Item> {
let obj: *mut AnyObject = unsafe { msg_send![self.0, nextObject] };
(!obj.is_null()).then_some(obj)
}
}