From 22f96bb2380b147f7db3b6371a529fe59b4367b2 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 4 Feb 2021 13:34:12 -0800 Subject: [PATCH] More general updates. - Filesystem Save/Open panels can return PathBuf's instead of Url's, which feels more native to Rust. - Support for drawing into an Image context with Core Graphics. - ListView swipe-to-reveal action support. - Experimental ListView cell reuse support. - Updates to QuickLook to also support PathBuf's. --- Cargo.toml | 8 ++- src/color.rs | 11 +++ src/filesystem/select.rs | 12 ++-- src/image/image.rs | 146 +++++++++++++++++++++++++++++++++++++- src/image/mod.rs | 4 +- src/listview/actions.rs | 107 ++++++++++++++++++++++++++++ src/listview/enums.rs | 40 ++++++++--- src/listview/macos.rs | 35 +++++++-- src/listview/mod.rs | 118 +++++++++++++++++++++++++++--- src/listview/row/macos.rs | 20 +++++- src/listview/row/mod.rs | 79 +++++++++++++++++++-- src/listview/traits.rs | 8 ++- src/pasteboard/mod.rs | 31 ++++++++ src/quicklook/config.rs | 12 ++-- src/quicklook/mod.rs | 8 +-- src/utils.rs | 4 ++ 16 files changed, 590 insertions(+), 53 deletions(-) create mode 100644 src/listview/actions.rs diff --git a/Cargo.toml b/Cargo.toml index 1a31b8e..92e40e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,13 @@ repository = "https://github.com/ryanmcgrath/cacao" categories = ["gui", "os::macos-apis", "os::ios-apis"] keywords = ["gui", "macos", "ios", "appkit", "uikit"] +[package.metadata.docs.rs] +default-target = "x86_64-apple-darwin" + [dependencies] block = "0.1.6" -core-foundation = { version = "0.7", features = ["with-chrono"] } -core-graphics = "0.19.0" +core-foundation = { version = "0.9", features = ["with-chrono", "mac_os_10_8_features"] } +core-graphics = "0.22" dispatch = "0.2.0" lazy_static = "1.4.0" libc = "0.2" @@ -26,6 +29,7 @@ default = ["macos"] cloudkit = [] ios = [] macos = [] +quicklook = [] user-notifications = ["uuid"] webview = [] webview-downloading = [] diff --git a/src/color.rs b/src/color.rs index c08c912..c5300be 100644 --- a/src/color.rs +++ b/src/color.rs @@ -2,6 +2,7 @@ //! for (what I believe) is a friendlier API. use core_graphics::base::CGFloat; +use core_graphics::color::CGColor; use objc::{class, msg_send, sel, sel_impl}; @@ -69,6 +70,16 @@ impl Color { } } + /// Maps to CGColor, used across platforms. + pub fn cg_color(&self) -> CGColor { + let red = self.red as CGFloat / 255.0; + let green = self.green as CGFloat / 255.0; + let blue = self.blue as CGFloat / 255.0; + let alpha = self.alpha as CGFloat / 255.0; + + CGColor::rgb(red, green, blue, alpha) + } + /// Returns the red channel in a floating point number form, from 0 to 1. #[inline] pub fn red_f32(&self) -> f32 { diff --git a/src/filesystem/select.rs b/src/filesystem/select.rs index be54fe1..932c941 100644 --- a/src/filesystem/select.rs +++ b/src/filesystem/select.rs @@ -2,6 +2,8 @@ //! urls to work with. It currently doesn't implement _everything_ necessary, but it's functional //! enough for general use. +use std::path::PathBuf; + use block::ConcreteBlock; use objc::{class, msg_send, sel, sel_impl}; @@ -120,7 +122,7 @@ impl FileSelectPanel { /// Note that this clones the underlying `NSOpenPanel` pointer. This is theoretically safe as /// the system runs and manages that in another process, and we're still abiding by the general /// retain/ownership rules here. - pub fn show) + 'static>(&self, handler: F) { + pub fn show) + 'static>(&self, handler: F) { let panel = self.panel.clone(); let completion = ConcreteBlock::new(move |result: NSInteger| { let response: ModalResponse = result.into(); @@ -140,8 +142,8 @@ impl FileSelectPanel { /// Retrieves the selected URLs from the provided panel. /// This is currently a bit ugly, but it's also not something that needs to be the best thing in /// the world as it (ideally) shouldn't be called repeatedly in hot spots. -pub fn get_urls(panel: &Object) -> Vec { - let mut paths: Vec = vec![]; +pub fn get_urls(panel: &Object) -> Vec { + let mut paths: Vec = vec![]; unsafe { let urls: id = msg_send![&*panel, URLs]; @@ -153,8 +155,8 @@ pub fn get_urls(panel: &Object) -> Vec { } let url: id = msg_send![urls, objectAtIndex:count-1]; - let path = NSString::wrap(msg_send![url, absoluteString]).to_str().to_string(); - paths.push(path); + let path = NSString::wrap(msg_send![url, path]).to_str().to_string(); + paths.push(path.into()); count -= 1; } } diff --git a/src/image/image.rs b/src/image/image.rs index 700eabf..396dc69 100644 --- a/src/image/image.rs +++ b/src/image/image.rs @@ -1,16 +1,158 @@ use objc_id::ShareId; use objc::runtime::Object; -use crate::foundation::{id}; +use objc::{class, msg_send, sel, sel_impl}; + +use block::ConcreteBlock; + +use core_graphics::{ + base::{CGFloat}, + geometry::{CGRect, CGPoint, CGSize} +}; +use core_graphics::context::{CGContext, CGContextRef}; + +use crate::foundation::{id, YES, NO}; + +#[derive(Debug)] +pub enum ResizeBehavior { + AspectFit, + AspectFill, + Stretch, + Center +} + +fn max_cgfloat(x: CGFloat, y: CGFloat) -> CGFloat { + if x == y { return x; } + + match x > y { + true => x, + false => y + } +} + +fn min_cgfloat(x: CGFloat, y: CGFloat) -> CGFloat { + if x == y { return x; } + + match x < y { + true => x, + false => y + } +} + +impl ResizeBehavior { + pub fn apply(&self, source: CGRect, target: CGRect) -> CGRect { + // if equal, just return source + if( + source.origin.x == target.origin.x && + source.origin.y == target.origin.y && + source.size.width == target.size.width && + source.size.height == target.size.height + ) { + return source; + } + + if( + source.origin.x == 0. && + source.origin.y == 0. && + source.size.width == 0. && + source.size.height == 0. + ) { + return source; + } + + let mut scales = CGSize::new(0., 0.); + scales.width = (target.size.width / source.size.width).abs(); + scales.height = (target.size.height / source.size.height).abs(); + + match self { + ResizeBehavior::AspectFit => { + scales.width = min_cgfloat(scales.width, scales.height); + scales.height = scales.width; + }, + + ResizeBehavior::AspectFill => { + scales.width = max_cgfloat(scales.width, scales.height); + scales.height = scales.width; + }, + + ResizeBehavior::Stretch => { /* will do this as default */ }, + + ResizeBehavior::Center => { + scales.width = 1.; + scales.height = 1.; + } + } + + let mut result = source; + result.size.width *= scales.width; + result.size.height *= scales.height; + result.origin.x = target.origin.x + (target.size.width - result.size.width) / 2.; + result.origin.y = target.origin.y + (target.size.height - result.size.height) / 2.; + result + } +} + +#[derive(Debug)] +pub struct DrawConfig { + pub source: (CGFloat, CGFloat), + pub target: (CGFloat, CGFloat), + pub resize: ResizeBehavior +} #[derive(Clone, Debug)] pub struct Image(pub ShareId); impl Image { + /// Wraps a system-returned image, e.g from QuickLook previews. pub fn with(image: id) -> Self { Image(unsafe { ShareId::from_ptr(image) }) } -} + /// Draw a custom image and get it back as a returned `Image`. + pub fn draw(config: DrawConfig, handler: F) -> Self + where + F: Fn(CGRect, &CGContextRef) -> bool + 'static + { + let source_frame = CGRect::new( + &CGPoint::new(0., 0.), + &CGSize::new(config.source.0, config.source.1) + ); + + let target_frame = CGRect::new( + &CGPoint::new(0., 0.), + &CGSize::new(config.target.0, config.target.1) + ); + + let resized_frame = config.resize.apply(source_frame, target_frame); + + let block = ConcreteBlock::new(move |_destination: CGRect| unsafe { + let current_context: id = msg_send![class!(NSGraphicsContext), currentContext]; + let context_ptr: core_graphics::sys::CGContextRef = msg_send![current_context, CGContext]; + let context = CGContext::from_existing_context_ptr(context_ptr); + let _: () = msg_send![class!(NSGraphicsContext), saveGraphicsState]; + + context.translate(resized_frame.origin.x, resized_frame.origin.y); + context.scale( + resized_frame.size.width / config.source.0, + resized_frame.size.height / config.source.1 + ); + + let result = handler(resized_frame, &context); + + let _: () = msg_send![class!(NSGraphicsContext), restoreGraphicsState]; + + match result { + true => YES, + false => NO + } + }); + let block = block.copy(); + + Image(unsafe { + let img: id = msg_send![class!(NSImage), imageWithSize:target_frame.size flipped:YES drawingHandler:block]; + ShareId::from_ptr(img) + }) + } +} diff --git a/src/image/mod.rs b/src/image/mod.rs index 6830da4..db6071d 100644 --- a/src/image/mod.rs +++ b/src/image/mod.rs @@ -19,7 +19,7 @@ mod ios; use ios::register_image_view_class; mod image; -pub use image::Image; +pub use image::{Image, DrawConfig, ResizeBehavior}; /// A helper method for instantiating view classes and applying default settings to them. fn allocate_view(registration_fn: fn() -> *const Class) -> id { @@ -37,7 +37,7 @@ fn allocate_view(registration_fn: fn() -> *const Class) -> id { /// A clone-able handler to a `ViewController` reference in the Objective C runtime. We use this /// instead of a stock `View` for easier recordkeeping, since it'll need to hold the `View` on that /// side anyway. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct ImageView { /// A pointer to the Objective-C runtime view controller. pub objc: ShareId, diff --git a/src/listview/actions.rs b/src/listview/actions.rs new file mode 100644 index 0000000..0990cd7 --- /dev/null +++ b/src/listview/actions.rs @@ -0,0 +1,107 @@ +use objc_id::Id; +use objc::runtime::Object; +use objc::{class, msg_send, sel, sel_impl}; + +use block::ConcreteBlock; + +use crate::color::Color; +use crate::foundation::{id, NSString, NSUInteger}; +use crate::image::Image; + +/// Represents the "type" or "style" of row action. A `Regular` action is +/// nothing special; do whatever you want. A `Destructive` action will have +/// a special animation when being deleted, among any other system specialties. +#[derive(Debug)] +pub enum RowActionStyle { + /// The stock, standard, regular action. + Regular, + + /// Use this to denote that an action is destructive. + Destructive +} + +impl Default for RowActionStyle { + fn default() -> Self { + RowActionStyle::Regular + } +} + +impl From for NSUInteger { + fn from(style: RowActionStyle) -> Self { + match style { + RowActionStyle::Regular => 0, + RowActionStyle::Destructive => 1 + } + } +} + +/// Represents an action that can be displayed when a user swipes-to-reveal +/// on a ListViewRow. You return this from the appropriate delegate method, +/// and the system will handle displaying the necessary pieces for you. +#[derive(Debug)] +pub struct RowAction(pub Id); + +impl RowAction { + /// Creates and returns a new `RowAction`. You'd use this handler to + /// configure whatever action you want to show when a user swipes-to-reveal + /// on your ListViewRow. + /// + /// Additional configuration can be done after initialization, if need be. + pub fn new(title: &str, style: RowActionStyle, handler: F) -> Self + where + F: Fn(RowAction, usize) + Send + Sync + 'static + { + let title = NSString::new(title); + let block = ConcreteBlock::new(move |action: id, row: NSUInteger| { + let action = RowAction(unsafe { + Id::from_ptr(action) + }); + + handler(action, row as usize); + }); + let block = block.copy(); + let style = style as NSUInteger; + + RowAction(unsafe { + let cls = class!(NSTableViewRowAction); + Id::from_ptr(msg_send![cls, rowActionWithStyle:style + title:title.into_inner() + handler:block + ]) + }) + } + + /// Sets the title of this action. + pub fn set_title(&mut self, title: &str) { + let title = NSString::new(title); + + unsafe { + let _: () = msg_send![&*self.0, setTitle:title.into_inner()]; + } + } + + /// Sets the background color of this action. + pub fn set_background_color(&mut self, color: Color) { + let color = color.into_platform_specific_color(); + + unsafe { + let _: () = msg_send![&*self.0, setBackgroundColor:color]; + } + } + + /// Sets the style of this action. + pub fn set_style(&mut self, style: RowActionStyle) { + let style = style as NSUInteger; + + unsafe { + let _: () = msg_send![&*self.0, setStyle:style]; + } + } + + /// Sets an optional image for this action. + pub fn set_image(&mut self, image: Image) { + unsafe { + let _: () = msg_send![&*self.0, setImage:&*image.0]; + } + } +} diff --git a/src/listview/enums.rs b/src/listview/enums.rs index 1485946..b6ad30e 100644 --- a/src/listview/enums.rs +++ b/src/listview/enums.rs @@ -1,9 +1,9 @@ -use crate::foundation::NSUInteger; +use crate::foundation::{NSInteger, NSUInteger}; /// This enum represents the different stock animations possible /// for ListView row operations. You can pass it to `insert_rows` /// and `remove_rows` - reloads don't get animations. -pub enum ListViewAnimation { +pub enum RowAnimation { /// No animation. None, @@ -27,16 +27,36 @@ pub enum ListViewAnimation { SlideRight } -impl Into for ListViewAnimation { +impl Into for RowAnimation { fn into(self) -> NSUInteger { match self { - ListViewAnimation::None => 0x0, - ListViewAnimation::Fade => 0x1, - ListViewAnimation::Gap => 0x2, - ListViewAnimation::SlideUp => 0x10, - ListViewAnimation::SlideDown => 0x20, - ListViewAnimation::SlideLeft => 0x30, - ListViewAnimation::SlideRight => 0x40 + RowAnimation::None => 0x0, + RowAnimation::Fade => 0x1, + RowAnimation::Gap => 0x2, + RowAnimation::SlideUp => 0x10, + RowAnimation::SlideDown => 0x20, + RowAnimation::SlideLeft => 0x30, + RowAnimation::SlideRight => 0x40 + } + } +} + +#[derive(Debug)] +pub enum RowEdge { + Leading, + Trailing +} + +impl Into for NSInteger { + fn into(self) -> RowEdge { + match self { + 0 => RowEdge::Leading, + 1 => RowEdge::Trailing, + + // @TODO: This *should* be unreachable, provided macOS doesn't start + // letting people swipe from vertical directions to reveal stuff. Have to + // feel like there's a better way to do this, though... + _ => { unreachable!(); } } } } diff --git a/src/listview/macos.rs b/src/listview/macos.rs index 2e9db14..94eae4d 100644 --- a/src/listview/macos.rs +++ b/src/listview/macos.rs @@ -14,9 +14,12 @@ use objc::runtime::{Class, Object, Sel, BOOL}; use objc::{class, sel, sel_impl, msg_send}; use objc_id::Id; -use crate::foundation::{id, YES, NO, NSInteger, NSUInteger}; +use crate::foundation::{id, YES, NO, NSArray, NSInteger, NSUInteger}; use crate::dragdrop::DragInfo; -use crate::listview::{LISTVIEW_DELEGATE_PTR, ListViewDelegate}; +use crate::listview::{ + LISTVIEW_DELEGATE_PTR, LISTVIEW_CELL_VENDOR_PTR, + ListViewDelegate, RowEdge +}; use crate::utils::load; /// Determines the number of items by way of the backing data source (the Rust struct). @@ -32,12 +35,12 @@ extern fn number_of_items( extern fn view_for_column( this: &Object, _: Sel, - table_view: id, + _table_view: id, _: id, item: NSInteger ) -> id { let view = load::(this, LISTVIEW_DELEGATE_PTR); - let item = view.item(item as usize); + let item = view.item_for(item as usize); // A hacky method of returning the underlying pointer // without Rust annoying us. @@ -46,10 +49,30 @@ extern fn view_for_column( // as we *know* the underlying view will be retained by the NSTableView, so // passing over one more won't really screw up retain counts. unsafe { - msg_send![&*item, self] + msg_send![&*item.objc, self] } } +extern fn row_actions_for_row( + this: &Object, + _: Sel, + _table_view: id, + row: NSInteger, + edge: NSInteger +) -> id { + let edge: RowEdge = edge.into(); + let view = load::(this, LISTVIEW_DELEGATE_PTR); + + let actions = view.actions_for(row as usize, edge); + + //if actions.len() > 0 { + let ids: Vec<&Object> = actions.iter().map(|action| &*action.0).collect(); + NSArray::from(ids).into_inner() + //} else { + // NSArray::new(&[]).into_inner() + //} +} + /// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though. extern fn enforce_normalcy(_: &Object, _: Sel) -> BOOL { return YES; @@ -140,12 +163,14 @@ pub(crate) fn register_listview_class_with_delegate() -> *c // A pointer to the "view controller" on the Rust side. It's expected that this doesn't // move. decl.add_ivar::(LISTVIEW_DELEGATE_PTR); + decl.add_ivar::(LISTVIEW_CELL_VENDOR_PTR); decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL); // Tableview-specific decl.add_method(sel!(numberOfRowsInTableView:), number_of_items:: as extern fn(&Object, _, id) -> NSInteger); decl.add_method(sel!(tableView:viewForTableColumn:row:), view_for_column:: as extern fn(&Object, _, id, id, NSInteger) -> id); + decl.add_method(sel!(tableView:rowActionsForRow:edge:), row_actions_for_row:: as extern fn(&Object, _, id, NSInteger, NSInteger) -> id); // Drag and drop operations (e.g, accepting files) decl.add_method(sel!(draggingEntered:), dragging_entered:: as extern fn (&mut Object, _, _) -> NSUInteger); diff --git a/src/listview/mod.rs b/src/listview/mod.rs index f7a3465..0bb6e8e 100644 --- a/src/listview/mod.rs +++ b/src/listview/mod.rs @@ -41,6 +41,8 @@ //! //! For more information on Autolayout, view the module or check out the examples folder. +use std::collections::HashMap; + use core_graphics::base::CGFloat; use objc_id::ShareId; use objc::runtime::{Class, Object}; @@ -66,7 +68,7 @@ mod ios; use ios::{register_view_class, register_view_class_with_delegate}; mod enums; -pub use enums::ListViewAnimation; +pub use enums::{RowAnimation, RowEdge}; mod traits; pub use traits::ListViewDelegate; @@ -74,7 +76,68 @@ pub use traits::ListViewDelegate; mod row; pub use row::ListViewRow; +mod actions; +pub use actions::{RowAction, RowActionStyle}; + pub(crate) static LISTVIEW_DELEGATE_PTR: &str = "rstListViewDelegatePtr"; +pub(crate) static LISTVIEW_CELL_VENDOR_PTR: &str = "rstListViewCellVendorPtr"; + +use std::any::Any; +use std::sync::{Arc, RwLock}; + +use std::rc::Rc; +use std::cell::RefCell; + +use crate::view::ViewDelegate; + +pub(crate) type CellFactoryMap = HashMap<&'static str, Box Box>>; + +#[derive(Clone)] +pub struct CellFactory(pub Rc>); + +impl std::fmt::Debug for CellFactory { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CellFactory").finish() + } +} + +impl CellFactory { + pub fn new() -> Self { + CellFactory(Rc::new(RefCell::new(HashMap::new()))) + } + + pub fn insert(&self, identifier: &'static str, vendor: F) + where + F: Fn() -> T + 'static, + T: ViewDelegate + 'static + { + let mut lock = self.0.borrow_mut(); + lock.insert(identifier, Box::new(move || { + let cell = vendor(); + Box::new(cell) as Box + })); + } + + pub fn get(&self, identifier: &'static str) -> Box + where + R: ViewDelegate + 'static + { + let lock = self.0.borrow(); + let vendor = match lock.get(identifier) { + Some(v) => v, + None => { + panic!("Unable to dequeue cell of type {}: did you forget to register it?", identifier); + } + }; + let view = vendor(); + + if let Ok(view) = view.downcast::() { + view + } else { + panic!("Asking for cell of type {}, but failed to match the type!", identifier); + } + } +} /// A helper method for instantiating view classes and applying default settings to them. fn allocate_view(registration_fn: fn() -> *const Class) -> id { @@ -110,6 +173,10 @@ fn allocate_view(registration_fn: fn() -> *const Class) -> id { #[derive(Debug)] pub struct ListView { + /// Internal map of cell identifers/vendors. These are used for handling dynamic cell + /// allocation and reuse, which is necessary for an "infinite" listview. + cell_factory: CellFactory, + /// A pointer to the Objective-C runtime view controller. pub objc: ShareId, @@ -176,6 +243,7 @@ impl ListView { let anchor_view = view; ListView { + cell_factory: CellFactory::new(), delegate: None, top: LayoutAnchorY::new(unsafe { msg_send![anchor_view, topAnchor] }), leading: LayoutAnchorX::new(unsafe { msg_send![anchor_view, leadingAnchor] }), @@ -193,19 +261,21 @@ impl ListView { } } - impl ListView where T: ListViewDelegate + 'static { /// Initializes a new View with a given `ViewDelegate`. This enables you to respond to events /// and customize the view as a module, similar to class-based systems. pub fn with(delegate: T) -> ListView { let mut delegate = Box::new(delegate); + let cell = CellFactory::new(); let view = allocate_view(register_listview_class_with_delegate::); unsafe { //let view: id = msg_send![register_view_class_with_delegate::(), new]; //let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; - let ptr: *const T = &*delegate; - (&mut *view).set_ivar(LISTVIEW_DELEGATE_PTR, ptr as usize); + let delegate_ptr: *const T = &*delegate; + let cell_vendor_ptr: *const RefCell = &*cell.0; + (&mut *view).set_ivar(LISTVIEW_DELEGATE_PTR, delegate_ptr as usize); + (&mut *view).set_ivar(LISTVIEW_CELL_VENDOR_PTR, cell_vendor_ptr as usize); let _: () = msg_send![view, setDelegate:view]; let _: () = msg_send![view, setDataSource:view]; }; @@ -229,6 +299,7 @@ impl ListView where T: ListViewDelegate + 'static { let anchor_view = view; let mut view = ListView { + cell_factory: cell, delegate: None, top: LayoutAnchorY::new(unsafe { msg_send![anchor_view, topAnchor] }), leading: LayoutAnchorX::new(unsafe { msg_send![anchor_view, leadingAnchor] }), @@ -257,6 +328,7 @@ impl ListView { /// delegate - the `View` is the only true holder of those. pub(crate) fn clone_as_handle(&self) -> ListView { ListView { + cell_factory: CellFactory::new(), delegate: None, top: self.top.clone(), leading: self.leading.clone(), @@ -273,6 +345,34 @@ impl ListView { } } + /// Register a cell/row vendor function with an identifier. This is stored internally and used + /// for row-reuse. + pub fn register(&self, identifier: &'static str, vendor: F) + where + F: Fn() -> R + 'static, + R: ViewDelegate + 'static + { + self.cell_factory.insert(identifier, vendor); + } + + /// Dequeue a reusable cell. If one is not in the queue, will create and cache one for reuse. + pub fn dequeue(&self, identifier: &'static str) -> ListViewRow { + #[cfg(target_os = "macos")] + unsafe { + let key = NSString::new(identifier).into_inner(); + let cell: id = msg_send![&*self.objc, makeViewWithIdentifier:key owner:nil]; + + if cell != nil { + ListViewRow::from_cached(cell) + } else { + let delegate: Box = self.cell_factory.get(identifier); + let view = ListViewRow::with_boxed(delegate); + view.set_identifier(identifier); + view + } + } + } + /// Call this to set the background color for the backing layer. pub fn set_background_color(&self, color: Color) { let bg = color.into_platform_specific_color(); @@ -296,7 +396,7 @@ impl ListView { } } - pub fn insert_rows>(&self, indexes: I, animations: ListViewAnimation) { + pub fn insert_rows>(&self, indexes: I, animation: RowAnimation) { #[cfg(target_os = "macos")] unsafe { let index_set: id = msg_send![class!(NSMutableIndexSet), new]; @@ -306,12 +406,12 @@ impl ListView { let _: () = msg_send![index_set, addIndex:x]; } - let animation_options: NSUInteger = animations.into(); + let animation_options: NSUInteger = animation.into(); // We need to temporarily retain this; it can drop after the underlying NSTableView // has also retained it. let x = ShareId::from_ptr(index_set); - let _: () = msg_send![&*self.objc, insertRowsAtIndexes:&*x withAnimation:20]; + let _: () = msg_send![&*self.objc, insertRowsAtIndexes:&*x withAnimation:animation_options]; } } @@ -333,7 +433,7 @@ impl ListView { } } - pub fn remove_rows>(&self, indexes: I, animations: ListViewAnimation) { + pub fn remove_rows>(&self, indexes: I, animations: RowAnimation) { #[cfg(target_os = "macos")] unsafe { let index_set: id = msg_send![class!(NSMutableIndexSet), new]; @@ -348,7 +448,7 @@ impl ListView { // We need to temporarily retain this; it can drop after the underlying NSTableView // has also retained it. let x = ShareId::from_ptr(index_set); - let _: () = msg_send![&*self.objc, removeRowsAtIndexes:&*x withAnimation:20]; + let _: () = msg_send![&*self.objc, removeRowsAtIndexes:&*x withAnimation:animation_options]; } } diff --git a/src/listview/row/macos.rs b/src/listview/row/macos.rs index 01183c4..d908b6a 100644 --- a/src/listview/row/macos.rs +++ b/src/listview/row/macos.rs @@ -11,7 +11,7 @@ use std::sync::Once; use objc::declare::ClassDecl; use objc::runtime::{Class, Object, Sel, BOOL}; -use objc::{class, sel, sel_impl}; +use objc::{class, msg_send, sel, sel_impl}; use objc_id::Id; use crate::foundation::{id, YES, NO, NSUInteger}; @@ -74,6 +74,21 @@ extern fn dragging_exited(this: &mut Object, _: Sel, info: id) }); } +/// Normally, you might not want to do a custom dealloc override. However, reusable cells are +/// tricky - since we "forget" them when we give them to the system, we need to make sure to do +/// proper cleanup then the backing (cached) version is deallocated on the Objective-C side. Since +/// we know +extern fn dealloc(this: &Object, _: Sel) { + // Load the Box pointer here, and just let it drop normally. + unsafe { + let ptr: usize = *(&*this).get_ivar(LISTVIEW_ROW_DELEGATE_PTR); + let obj = ptr as *mut T; + let _x = Box::from_raw(obj); + + let _: () = msg_send![super(this, class!(NSView)), dealloc]; + } +} + /// Injects an `NSView` subclass. This is used for the default views that don't use delegates - we /// have separate classes here since we don't want to waste cycles on methods that will never be /// used if there's no delegates. @@ -116,6 +131,9 @@ pub(crate) fn register_listview_row_class_with_delegate() -> *c decl.add_method(sel!(concludeDragOperation:), conclude_drag_operation:: as extern fn (&mut Object, _, _)); decl.add_method(sel!(draggingExited:), dragging_exited:: as extern fn (&mut Object, _, _)); + // Cleanup + decl.add_method(sel!(dealloc), dealloc:: as extern fn (&Object, _)); + VIEW_CLASS = decl.register(); }); diff --git a/src/listview/row/mod.rs b/src/listview/row/mod.rs index b8145c6..5cf80a0 100644 --- a/src/listview/row/mod.rs +++ b/src/listview/row/mod.rs @@ -141,11 +141,49 @@ impl ListViewRow { } impl ListViewRow where T: ViewDelegate + 'static { + /// When we're able to retrieve a reusable view cell from the backing table view, we can check + /// for the pointer and attempt to reconstruct the ListViewRow that corresponds to this. + /// + /// We can be reasonably sure that the pointer for the delegate is accurate, as: + /// + /// - A `ListViewRow` is explicitly not clone-able + /// - It owns the Delegate on creation + /// - It takes ownership of the returned row in row_for_item + /// - When it takes ownership, it "forgets" the pointer - and the `dealloc` method on the + /// backing view cell will clean it up whenever it's dropped. + pub(crate) fn from_cached(view: id) -> ListViewRow { + let delegate = unsafe { + let ptr: usize = *(&*view).get_ivar(LISTVIEW_ROW_DELEGATE_PTR); + let obj = ptr as *mut T; + Box::from_raw(obj) + //&*obj + }; + //let delegate = crate::utils::load::(&*view, LISTVIEW_ROW_DELEGATE_PTR); + + let mut view = ListViewRow { + delegate: Some(delegate), + top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }), + leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }), + trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }), + bottom: LayoutAnchorY::new(unsafe { msg_send![view, bottomAnchor] }), + width: LayoutAnchorDimension::new(unsafe { msg_send![view, widthAnchor] }), + height: LayoutAnchorDimension::new(unsafe { msg_send![view, heightAnchor] }), + center_x: LayoutAnchorX::new(unsafe { msg_send![view, centerXAnchor] }), + center_y: LayoutAnchorY::new(unsafe { msg_send![view, centerYAnchor] }), + objc: unsafe { ShareId::from_ptr(view) }, + }; + + view + } + + pub fn with(delegate: T) -> ListViewRow { + let delegate = Box::new(delegate); + Self::with_boxed(delegate) + } + /// Initializes a new View with a given `ViewDelegate`. This enables you to respond to events /// and customize the view as a module, similar to class-based systems. - pub fn with(delegate: T) -> ListViewRow { - let mut delegate = Box::new(delegate); - + pub fn with_boxed(mut delegate: Box) -> ListViewRow { let view = allocate_view(register_listview_row_class_with_delegate::); unsafe { //let view: id = msg_send![register_view_class_with_delegate::(), new]; @@ -171,13 +209,35 @@ impl ListViewRow where T: ViewDelegate + 'static { view.delegate = Some(delegate); view } + + pub fn wut(mut self) -> ListViewRow { + // "forget" delegate, then move into standard ListViewRow + // to ease return type + let delegate = self.delegate.take(); + if let Some(d) = delegate { + let _ = Box::into_raw(d); + } + + ListViewRow { + delegate: None, + top: self.top.clone(), + leading: self.leading.clone(), + trailing: self.trailing.clone(), + bottom: self.bottom.clone(), + width: self.width.clone(), + height: self.height.clone(), + center_x: self.center_x.clone(), + center_y: self.center_y.clone(), + objc: self.objc.clone() + } + } } -impl From<&ListViewRow> for ShareId { +/*impl From<&ListViewRow> for ShareId { fn from(row: &ListViewRow) -> ShareId { row.objc.clone() } -} +}*/ impl ListViewRow { /// An internal method that returns a clone of this object, sans references to the delegate or @@ -199,6 +259,15 @@ impl ListViewRow { } } + /// Sets the identifier, which enables cells to be reused and dequeued properly. + pub fn set_identifier(&self, identifier: &'static str) { + let identifier = NSString::new(identifier).into_inner(); + + unsafe { + let _: () = msg_send![&*self.objc, setIdentifier:identifier]; + } + } + /// Call this to set the background color for the backing layer. pub fn set_background_color(&self, color: Color) { let bg = color.into_platform_specific_color(); diff --git a/src/listview/traits.rs b/src/listview/traits.rs index 9df00e3..6f95f98 100644 --- a/src/listview/traits.rs +++ b/src/listview/traits.rs @@ -2,7 +2,7 @@ use crate::Node; use crate::dragdrop::{DragInfo, DragOperation}; -use crate::listview::{ListView, ListViewRow}; +use crate::listview::{ListView, ListViewRow, RowAction, RowEdge}; use crate::layout::Layout; use crate::view::View; @@ -19,7 +19,11 @@ pub trait ListViewDelegate { /// choose to try and work with this. NSTableView & such associated delegate patterns /// are tricky to support in Rust, and while I have a few ideas about them, I haven't /// had time to sit down and figure them out properly yet. - fn item(&self, _row: usize) -> Node; + fn item_for(&self, _row: usize) -> ListViewRow; + + /// An optional delegate method; implement this if you'd like swipe-to-reveal to be + /// supported for a given row by returning a vector of actions to show. + fn actions_for(&self, row: usize, edge: RowEdge) -> Vec { Vec::new() } /// Called when this is about to be added to the view heirarchy. fn will_appear(&self, _animated: bool) {} diff --git a/src/pasteboard/mod.rs b/src/pasteboard/mod.rs index b6d7c7a..5e1f39d 100644 --- a/src/pasteboard/mod.rs +++ b/src/pasteboard/mod.rs @@ -2,6 +2,8 @@ //! (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 std::path::PathBuf; + use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; use objc_id::ShareId; @@ -91,4 +93,33 @@ impl Pasteboard { Ok(urls) } } + + /// Looks inside the pasteboard contents and extracts what FileURLs are there, if any. + pub fn get_file_paths(&self) -> Result, Box> { + unsafe { + let class: id = msg_send![class!(NSURL), class]; + let classes = NSArray::new(&[class]); + let contents: id = msg_send![&*self.0, 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(Error { + code: 666, + domain: "com.cacao-rs.pasteboard".to_string(), + description: "Pasteboard server returned no data.".to_string() + })); + } + + let urls = NSArray::wrap(contents).map(|url| { + let path = NSString::wrap(msg_send![url, path]).to_str().to_string(); + PathBuf::from(path) + }).into_iter().collect(); + + Ok(urls) + } + } } diff --git a/src/quicklook/config.rs b/src/quicklook/config.rs index cf1c981..75b14b1 100644 --- a/src/quicklook/config.rs +++ b/src/quicklook/config.rs @@ -1,10 +1,10 @@ +use std::path::Path; + use core_graphics::base::CGFloat; use objc::runtime::{Object}; use objc::{class, msg_send, sel, sel_impl}; use objc_id::ShareId; -use url::Url; - use crate::foundation::{id, YES, NSString, NSUInteger}; use crate::utils::CGSize; @@ -97,8 +97,8 @@ impl Default for ThumbnailConfig { impl ThumbnailConfig { /// Consumes the request and returns a native representation /// (`QLThumbnailGenerationRequest`). - pub fn to_request(self, url: &Url) -> id { - let file = NSString::new(url.as_str()); + pub fn to_request(self, path: &Path) -> id { + let file = NSString::new(path.to_str().unwrap()); let mut types: NSUInteger = 0; for mask in self.types { @@ -108,8 +108,8 @@ impl ThumbnailConfig { unsafe { let size = CGSize::new(self.size.0, self.size.1); - let from_url: id = msg_send![class!(NSURL), URLWithString:file.into_inner()]; - + // @TODO: Check nil here, or other bad conversion + let from_url: id = msg_send![class!(NSURL), fileURLWithPath:file.into_inner()]; let request: id = msg_send![class!(QLThumbnailGenerationRequest), alloc]; let request: id = msg_send![request, initWithFileAtURL:from_url diff --git a/src/quicklook/mod.rs b/src/quicklook/mod.rs index beb1a38..662b3e9 100644 --- a/src/quicklook/mod.rs +++ b/src/quicklook/mod.rs @@ -1,11 +1,11 @@ +use std::path::Path; + use objc::runtime::{Object}; use objc::{class, msg_send, sel, sel_impl}; use objc_id::ShareId; use block::ConcreteBlock; -use url::Url; - use crate::error::Error; use crate::foundation::{id, nil, NSUInteger}; use crate::image::Image; @@ -23,7 +23,7 @@ impl ThumbnailGenerator { }) } - pub fn generate(&self, url: &Url, config: ThumbnailConfig, callback: F) + pub fn generate(&self, path: &Path, config: ThumbnailConfig, callback: F) where F: Fn(Result<(Image, ThumbnailQuality), Error>) + Send + Sync + 'static { @@ -41,7 +41,7 @@ impl ThumbnailGenerator { }); let block = block.copy(); - let request = config.to_request(url); + let request = config.to_request(path); unsafe { let _: () = msg_send![&*self.0, generateRepresentationsForRequest:request diff --git a/src/utils.rs b/src/utils.rs index ea508ba..178bd44 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -71,6 +71,10 @@ impl CGSize { pub fn new(width: CGFloat, height: CGFloat) -> Self { CGSize { width, height } } + + pub fn zero() -> Self { + CGSize { width: 0., height: 0. } + } } unsafe impl Encode for CGSize {