Ongoing efforts, experimenting with stack/heap issues (ObjC/NSObject/AppKit are... mostly heap already), further work on Pasteboard support

This commit is contained in:
Ryan McGrath 2020-03-10 20:09:24 -07:00
parent d68cbdc450
commit 80ba209413
No known key found for this signature in database
GPG key ID: 811674B62B666830
13 changed files with 283 additions and 62 deletions

View file

@ -16,6 +16,7 @@ lazy_static = "1"
objc = "0.2.7"
objc_id = "0.1.1"
uuid = { version = "0.8", features = ["v4"] }
url = "2.1.1"
[features]
enable-webview-downloading = []

View file

@ -5,8 +5,11 @@
use cocoa::foundation::NSUInteger;
use objc::runtime::Object;
use objc::{msg_send, sel, sel_impl};
use objc_id::Id;
use crate::pasteboard::Pasteboard;
/// Represents operations that can happen for a given drag/drop scenario.
pub enum DragOperation {
/// No drag operations are allowed.
@ -56,5 +59,13 @@ pub struct DragInfo {
}
impl DragInfo {
/// Returns a wrapped Pasteboard instance, enabling you to get the contents of whatever is
/// being pasted/dragged/dropped/etc.
///
/// Note: in general, you should not store pasteboards.
pub fn get_pasteboard(&self) -> Pasteboard {
unsafe {
Pasteboard::with(msg_send![&*self.info, draggingPasteboard])
}
}
}

51
appkit/src/error.rs Normal file
View file

@ -0,0 +1,51 @@
//! A wrapper for `NSError`, which can be (and is) bubbled up for certain calls in this library. It
//! attempts to be thread safe where possible, and extract the "default" usable information out of
//! an `NSError`. This might not be what you need, though, so if it's missing something... well,
//! it's up for discussion.
use std::error;
use std::fmt;
use cocoa::base::id;
use objc::{msg_send, sel, sel_impl};
use crate::utils::str_from;
/// A wrapper around pieces of data extracted from `NSError`. This could be improved: right now, it
/// allocates `String` instances when theoretically it could be avoided, and we might be erasing
/// certain parts of the `NSError` object that are useful.
#[derive(Clone, Debug)]
pub struct AppKitError {
pub code: usize,
pub domain: String,
pub description: String
}
impl AppKitError {
/// Given an `NSError` (i.e, an id reference) we'll pull out the relevant information and
/// configure this. We pull out the information as it makes the error thread safe this way,
/// which is... easier, in some cases.
pub fn new(error: id) -> Box<Self> {
let (code, domain, description) = unsafe {
let code: usize = msg_send![error, code];
let domain: id = msg_send![error, domain];
let description: id = msg_send![error, localizedDescription];
(code, domain, description)
};
Box::new(AppKitError {
code: code,
domain: str_from(domain).to_string(),
description: str_from(description).to_string()
})
}
}
impl fmt::Display for AppKitError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.description)
}
}
impl error::Error for AppKitError {}

View file

@ -1,69 +1,92 @@
//! A wrapper for `NSFileManager`, which is necessary for macOS/iOS (the sandbox makes things
//! tricky, and this transparently handles it for you).
use std::rc::Rc;
use std::cell::RefCell;
use std::error::Error;
use std::sync::RwLock;
use cocoa::base::{id, nil, NO};
use cocoa::foundation::NSUInteger;
use cocoa::foundation::{NSString, NSUInteger};
use objc_id::Id;
use objc::runtime::Object;
use objc::runtime::{BOOL, Object};
use objc::{class, msg_send, sel, sel_impl};
use url::Url;
use crate::error::AppKitError;
use crate::filesystem::enums::{SearchPathDirectory, SearchPathDomainMask};
use crate::utils::str_from;
pub struct FileManagerInner {
pub manager: Id<Object>
pub struct FileManager {
pub manager: RwLock<Id<Object>>
}
impl Default for FileManagerInner {
impl Default for FileManager {
/// Returns a default file manager, which maps to the default system file manager. For common
/// and simple tasks, with no callbacks, you might want this.
fn default() -> Self {
FileManagerInner {
manager: unsafe {
FileManager {
manager: RwLock::new(unsafe {
let manager: id = msg_send![class!(NSFileManager), defaultManager];
Id::from_ptr(manager)
}
})
}
}
}
impl FileManagerInner {
pub fn get_path(&self, directory: SearchPathDirectory, in_domain: SearchPathDomainMask) -> Result<String, Box<dyn std::error::Error>> {
impl FileManager {
/// Returns a new FileManager that opts in to delegate methods.
pub fn new() -> Self {
FileManager {
manager: RwLock::new(unsafe {
let manager: id = msg_send![class!(NSFileManager), new];
Id::from_ptr(manager)
})
}
}
/// Given a directory/domain combination, will attempt to get the directory that matches.
/// Returns a PathBuf that wraps the given location. If there's an error on the Objective-C
/// side, we attempt to catch it and bubble it up.
pub fn get_directory(&self, directory: SearchPathDirectory, in_domain: SearchPathDomainMask) -> Result<Url, Box<dyn Error>> {
let dir: NSUInteger = directory.into();
let mask: NSUInteger = in_domain.into();
unsafe {
let dir: id = msg_send![&*self.manager, URLForDirectory:dir
let directory = unsafe {
let manager = self.manager.read().unwrap();
let dir: id = msg_send![&**manager, URLForDirectory:dir
inDomain:mask
appropriateForURL:nil
create:NO
error:nil];
let s: id = msg_send![dir, path];
Ok(str_from(s).to_string())
}
let s: id = msg_send![dir, absoluteString];
str_from(s)
};
Url::parse(directory).map_err(|e| e.into())
}
}
#[derive(Default)]
pub struct FileManager(Rc<RefCell<FileManagerInner>>);
/// Given two paths, moves file (`from`) to the location specified in `to`. This can result in
/// an error on the Objective-C side, which we attempt to handle and bubble up as a result if
/// so.
pub fn move_item(&self, from: Url, to: Url) -> Result<(), Box<dyn Error>> {
unsafe {
let s = NSString::alloc(nil).init_str(from.as_str());
let from_url: id = msg_send![class!(NSURL), URLWithString:s];
impl FileManager {
pub fn new() -> Self {
FileManager(Rc::new(RefCell::new(FileManagerInner {
manager: unsafe {
let manager: id = msg_send![class!(NSFileManager), new];
Id::from_ptr(manager)
let s2 = NSString::alloc(nil).init_str(to.as_str());
let to_url: id = msg_send![class!(NSURL), URLWithString:s2];
// This should potentially be write(), but the backing class handles this logic
// already, so... going to leave it as read.
let manager = self.manager.read().unwrap();
let error: id = nil;
let result: BOOL = msg_send![&**manager, moveItemAtURL:from_url toURL:to_url error:&error];
if result == NO {
return Err(AppKitError::new(error));
}
})))
}
}
pub fn get_path(&self, directory: SearchPathDirectory, in_domain: SearchPathDomainMask) -> Result<String, Box<dyn std::error::Error>> {
let manager = self.0.borrow();
manager.get_path(directory, in_domain)
Ok(())
}
//pub fn contents_of(directory: &str, properties: &[
}

View file

@ -27,6 +27,7 @@ pub mod color;
pub mod collection_view;
pub mod constants;
pub mod dragdrop;
pub mod error;
pub mod events;
pub mod filesystem;
pub mod geometry;
@ -40,6 +41,9 @@ pub mod view;
pub mod webview;
pub mod window;
// We re-export these so that they can be used without increasing build times.
pub use url;
pub mod prelude {
pub use crate::app::{App, AppDelegate};
@ -60,6 +64,6 @@ pub mod prelude {
pub use crate::view::{View, ViewController, ViewWrapper};
pub use appkit_derive::{
WindowWrapper
WindowWrapper, ViewWrapper
};
}

View file

@ -2,13 +2,18 @@
//! (think: drag and drop between applications). It exposes a Rust interface that tries to be
//! complete, but might not cover everything 100% right now - feel free to pull request.
use cocoa::base::id;
use std::error::Error;
use cocoa::base::{id, nil};
use cocoa::foundation::{NSArray};
use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl};
use objc_id::Id;
use url::Url;
use crate::pasteboard::types::PasteboardName;
use crate::error::AppKitError;
use crate::pasteboard::types::{PasteboardName, PasteboardType};
use crate::utils::str_from;
/// Represents an `NSPasteboard`, enabling you to handle copy/paste/drag and drop.
pub struct Pasteboard {
@ -32,7 +37,7 @@ impl Pasteboard {
}
}
/// Should be pasteboardname enum!
/// Retrieves the system Pasteboard for the given name/type.
pub fn named(name: PasteboardName) -> Self {
Pasteboard {
inner: unsafe {
@ -64,4 +69,60 @@ impl Pasteboard {
let _: () = msg_send![&*self.inner, clearContents];
}
}
/// Looks inside the pasteboard contents and extracts what FileURLs are there, if any.
pub fn get_file_urls(&self) -> Result<Vec<Url>, Box<dyn Error>> {
unsafe {
let mut i = 0;
let class: id = msg_send![class!(NSURL), class];
let classes: id = NSArray::arrayWithObjects(nil, &[class]);
let contents: id = msg_send![&*self.inner, readObjectsForClasses:classes options:nil];
// This can happen if the Pasteboard server has an error in returning items.
// In our case, we'll bubble up an error by checking the pasteboard.
if contents == nil {
// This error is not necessarily "correct", but in the event of an error in
// Pasteboard server retrieval I'm not sure where to check... and this stuff is
// kinda ancient and has conflicting docs in places. ;P
return Err(Box::new(AppKitError {
code: 666,
domain: "com.appkit-rs.pasteboard".to_string(),
description: "Pasteboard server returned no data.".to_string()
}));
}
let count: usize = msg_send![contents, count];
let mut urls: Vec<Url> = Vec::with_capacity(count);
loop {
let nsurl: id = msg_send![contents, objectAtIndex:i];
let path: id = msg_send![nsurl, path];
let s = str_from(path);
urls.push(Url::parse(&format!("file://{}", s))?);
i += 1;
if i == count { break; }
}
Ok(urls)
}
}
/*
/// Retrieves the pasteboard contents as a string. This can be `None` (`nil` on the Objective-C
/// side) if the pasteboard data doesn't match the requested type, so check accordingly.
///
/// Note: In macOS 10.6 and later, if the receiver contains multiple items that can provide string,
/// RTF, or RTFD data, the text data from each item is returned as a combined result separated by newlines.
/// This Rust wrapper is a quick pass, and could be improved. ;P
pub fn contents_for(&self, pasteboard_type: PasteboardType) -> Option<String> {
unsafe {
let contents: id = msg_send![&*self.inner, stringForType:pasteboard_type.to_nsstring()];
if contents != nil {
return Some(str_from(contents).to_string());
}
}
None
}*/
}

View file

@ -7,10 +7,19 @@ use cocoa::foundation::NSString;
/// Constants for the standard system pasteboard names.
#[derive(Debug, Copy, Clone)]
pub enum PasteboardName {
/// The dragging/dropping pasteboard.
Drag,
/// The find pasteboard.
Find,
/// The font pasteboard.
Font,
/// The general pasteboard.
General,
/// The ruler pasteboard.
Ruler
}
@ -19,11 +28,11 @@ impl PasteboardName {
pub fn to_nsstring(&self) -> id {
unsafe {
NSString::alloc(nil).init_str(match self {
PasteboardName::Drag => "",
PasteboardName::Find => "",
PasteboardName::Font => "",
PasteboardName::General => "",
PasteboardName::Ruler => ""
PasteboardName::Drag => "Apple CFPasteboard drag",
PasteboardName::Find => "Apple CFPasteboard find",
PasteboardName::Font => "Apple CFPasteboard font",
PasteboardName::General => "Apple CFPasteboard general",
PasteboardName::Ruler => "Apple CFPasteboard ruler"
})
}
}

View file

@ -15,8 +15,10 @@ use cocoa::foundation::{NSUInteger};
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{msg_send, sel, sel_impl};
use objc_id::Id;
use crate::constants::{BACKGROUND_COLOR, VIEW_CONTROLLER_PTR};
use crate::dragdrop::DragInfo;
use crate::view::traits::ViewController;
/// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though.
@ -37,21 +39,25 @@ extern fn update_layer(this: &Object, _: Sel) {
}
/// Called when a drag/drop operation has entered this view.
extern fn dragging_entered<T: ViewController>(this: &mut Object, _: Sel, _: id) -> NSUInteger {
extern fn dragging_entered<T: ViewController>(this: &mut Object, _: Sel, info: id) -> NSUInteger {
unsafe {
let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR);
let view = ptr as *const T;
(*view).dragging_entered().into()
(*view).dragging_entered(DragInfo {
info: Id::from_ptr(info)
}).into()
}
}
/// Called when a drag/drop operation has entered this view.
extern fn prepare_for_drag_operation<T: ViewController>(this: &mut Object, _: Sel, _: id) -> BOOL {
extern fn prepare_for_drag_operation<T: ViewController>(this: &mut Object, _: Sel, info: id) -> BOOL {
unsafe {
let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR);
let view = ptr as *const T;
match (*view).prepare_for_drag_operation() {
match (*view).prepare_for_drag_operation(DragInfo {
info: Id::from_ptr(info)
}) {
true => YES,
false => NO
}
@ -59,12 +65,14 @@ extern fn prepare_for_drag_operation<T: ViewController>(this: &mut Object, _: Se
}
/// Called when a drag/drop operation has entered this view.
extern fn perform_drag_operation<T: ViewController>(this: &mut Object, _: Sel, _: id) -> BOOL {
extern fn perform_drag_operation<T: ViewController>(this: &mut Object, _: Sel, info: id) -> BOOL {
unsafe {
let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR);
let view = ptr as *const T;
match (*view).perform_drag_operation() {
match (*view).perform_drag_operation(DragInfo {
info: Id::from_ptr(info)
}) {
true => YES,
false => NO
}
@ -72,11 +80,25 @@ extern fn perform_drag_operation<T: ViewController>(this: &mut Object, _: Sel, _
}
/// Called when a drag/drop operation has entered this view.
extern fn dragging_exited<T: ViewController>(this: &mut Object, _: Sel, _: id) {
extern fn conclude_drag_operation<T: ViewController>(this: &mut Object, _: Sel, info: id) {
unsafe {
let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR);
let view = ptr as *const T;
(*view).dragging_exited();
(*view).conclude_drag_operation(DragInfo {
info: Id::from_ptr(info)
});
}
}
/// Called when a drag/drop operation has entered this view.
extern fn dragging_exited<T: ViewController>(this: &mut Object, _: Sel, info: id) {
unsafe {
let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR);
let view = ptr as *const T;
(*view).dragging_exited(DragInfo {
info: Id::from_ptr(info)
});
}
}
@ -103,6 +125,7 @@ pub(crate) fn register_view_class<T: ViewController>() -> *const Class {
decl.add_method(sel!(draggingEntered:), dragging_entered::<T> as extern fn (&mut Object, _, _) -> NSUInteger);
decl.add_method(sel!(prepareForDragOperation:), prepare_for_drag_operation::<T> as extern fn (&mut Object, _, _) -> BOOL);
decl.add_method(sel!(performDragOperation:), perform_drag_operation::<T> as extern fn (&mut Object, _, _) -> BOOL);
decl.add_method(sel!(concludeDragOperation:), conclude_drag_operation::<T> as extern fn (&mut Object, _, _));
decl.add_method(sel!(draggingExited:), dragging_exited::<T> as extern fn (&mut Object, _, _));
VIEW_CLASS = decl.register();

View file

@ -3,7 +3,7 @@
use std::sync::Once;
use cocoa::base::{id, nil, YES, NO};
use cocoa::base::{id, NO};
use cocoa::foundation::{NSRect};
use objc::declare::ClassDecl;

View file

@ -4,7 +4,7 @@ use objc::runtime::Object;
use objc_id::ShareId;
use crate::dragdrop::DragOperation;
use crate::dragdrop::{DragInfo, DragOperation};
pub trait ViewWrapper {
fn get_handle(&self) -> Option<ShareId<Object>>;
@ -13,8 +13,19 @@ pub trait ViewWrapper {
pub trait ViewController {
fn did_load(&self);
fn dragging_entered(&self) -> DragOperation { DragOperation::None }
fn prepare_for_drag_operation(&self) -> bool { false }
fn perform_drag_operation(&self) -> bool { false }
fn dragging_exited(&self) {}
/// Invoked when the dragged image enters destination bounds or frame; returns dragging operation to perform.
fn dragging_entered(&self, _info: DragInfo) -> DragOperation { DragOperation::None }
/// Invoked when the image is released, allowing the receiver to agree to or refuse drag operation.
fn prepare_for_drag_operation(&self, _info: DragInfo) -> bool { false }
/// Invoked after the released image has been removed from the screen, signaling the receiver to import the pasteboard data.
fn perform_drag_operation(&self, _info: DragInfo) -> bool { false }
/// Invoked when the dragging operation is complete, signaling the receiver to perform any necessary clean-up.
fn conclude_drag_operation(&self, _info: DragInfo) {}
/// Invoked when the dragged image exits the destinations bounds rectangle (in the case of a view) or its frame
/// rectangle (in the case of a window object).
fn dragging_exited(&self, _info: DragInfo) {}
}

View file

@ -3,7 +3,7 @@
use std::rc::Rc;
use std::cell::RefCell;
use cocoa::base::{id, nil, YES, NO};
use cocoa::base::{id, nil, YES};
use cocoa::foundation::NSArray;
use objc_id::ShareId;
@ -86,3 +86,9 @@ impl View {
view.set_background_color(color);
}
}
impl std::fmt::Debug for View {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "View ({:p})", self)
}
}

View file

@ -35,7 +35,7 @@ impl Default for WindowConfig {
let dimensions = NSRect::new(NSPoint::new(0., 0.), NSSize::new(800., 600.));
let style = WindowStyle::Resizable | WindowStyle::Miniaturizable | WindowStyle::UnifiedTitleAndToolbar |
WindowStyle::Closable | WindowStyle::Titled;
WindowStyle::Closable | WindowStyle::Titled | WindowStyle::FullSizeContentView;
let alloc: id = msg_send![class!(NSWindow), alloc];
let window: id = msg_send![alloc, initWithContentRect:dimensions styleMask:style backing:2 as NSUInteger defer:YES];

View file

@ -7,13 +7,13 @@ use crate::proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
/// Derivces an `appkit::prelude::WinWrapper` block, which implements forwarding methods for things
/// Derivces an `appkit::prelude::WindowWrapper` block, which implements forwarding methods for things
/// like setting the window title, or showing and closing it. It currently expects that the wrapped
/// struct has `window` as the field holding the `Window` from `appkit-rs`.
///
/// Note that this expects that pointers to Window(s) should not move once created.
#[proc_macro_derive(WindowWrapper)]
pub fn impl_window_controller(input: TokenStream) -> TokenStream {
pub fn impl_window_wrapper(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
@ -30,3 +30,24 @@ pub fn impl_window_controller(input: TokenStream) -> TokenStream {
TokenStream::from(expanded)
}
/// Derives an `appkit::prelude::ViewWrapper` block, which implements some necessary bits and
/// pieces for View handling.
#[proc_macro_derive(ViewWrapper)]
pub fn impl_view_wrapper(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let generics = input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let expanded = quote! {
impl #impl_generics appkit::prelude::ViewWrapper for #name #ty_generics #where_clause {
fn get_handle(&self) -> Option<appkit::ShareId<appkit::Object>> {
self.view.get_handle()
}
}
};
TokenStream::from(expanded)
}