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.
This commit is contained in:
parent
62cebab691
commit
22f96bb238
|
@ -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 = []
|
||||
|
|
11
src/color.rs
11
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 {
|
||||
|
|
|
@ -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<F: Fn(Vec<String>) + 'static>(&self, handler: F) {
|
||||
pub fn show<F: Fn(Vec<PathBuf>) + '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<String> {
|
||||
let mut paths: Vec<String> = vec![];
|
||||
pub fn get_urls(panel: &Object) -> Vec<PathBuf> {
|
||||
let mut paths: Vec<PathBuf> = vec![];
|
||||
|
||||
unsafe {
|
||||
let urls: id = msg_send![&*panel, URLs];
|
||||
|
@ -153,8 +155,8 @@ pub fn get_urls(panel: &Object) -> Vec<String> {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Object>);
|
||||
|
||||
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<F>(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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Object>,
|
||||
|
|
107
src/listview/actions.rs
Normal file
107
src/listview/actions.rs
Normal file
|
@ -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<RowActionStyle> 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<Object>);
|
||||
|
||||
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<F>(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];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<NSUInteger> for ListViewAnimation {
|
||||
impl Into<NSUInteger> 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<RowEdge> 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!(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T: ListViewDelegate>(
|
|||
extern fn view_for_column<T: ListViewDelegate>(
|
||||
this: &Object,
|
||||
_: Sel,
|
||||
table_view: id,
|
||||
_table_view: id,
|
||||
_: id,
|
||||
item: NSInteger
|
||||
) -> id {
|
||||
let view = load::<T>(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<T: ListViewDelegate>(
|
|||
// 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<T: ListViewDelegate>(
|
||||
this: &Object,
|
||||
_: Sel,
|
||||
_table_view: id,
|
||||
row: NSInteger,
|
||||
edge: NSInteger
|
||||
) -> id {
|
||||
let edge: RowEdge = edge.into();
|
||||
let view = load::<T>(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<T: ListViewDelegate>() -> *c
|
|||
// A pointer to the "view controller" on the Rust side. It's expected that this doesn't
|
||||
// move.
|
||||
decl.add_ivar::<usize>(LISTVIEW_DELEGATE_PTR);
|
||||
decl.add_ivar::<usize>(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::<T> as extern fn(&Object, _, id) -> NSInteger);
|
||||
decl.add_method(sel!(tableView:viewForTableColumn:row:), view_for_column::<T> as extern fn(&Object, _, id, id, NSInteger) -> id);
|
||||
decl.add_method(sel!(tableView:rowActionsForRow:edge:), row_actions_for_row::<T> as extern fn(&Object, _, id, NSInteger, NSInteger) -> id);
|
||||
|
||||
// Drag and drop operations (e.g, accepting files)
|
||||
decl.add_method(sel!(draggingEntered:), dragging_entered::<T> as extern fn (&mut Object, _, _) -> NSUInteger);
|
||||
|
|
|
@ -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<Fn() -> Box<Any>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CellFactory(pub Rc<RefCell<CellFactoryMap>>);
|
||||
|
||||
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<F, T>(&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<Any>
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn get<R>(&self, identifier: &'static str) -> Box<R>
|
||||
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::<R>() {
|
||||
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<T = ()> {
|
||||
/// 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<Object>,
|
||||
|
||||
|
@ -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<T> ListView<T> 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<T> {
|
||||
let mut delegate = Box::new(delegate);
|
||||
let cell = CellFactory::new();
|
||||
|
||||
let view = allocate_view(register_listview_class_with_delegate::<T>);
|
||||
unsafe {
|
||||
//let view: id = msg_send![register_view_class_with_delegate::<T>(), 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<CellFactoryMap> = &*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<T> ListView<T> 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<T> ListView<T> {
|
|||
/// 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<T> ListView<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Register a cell/row vendor function with an identifier. This is stored internally and used
|
||||
/// for row-reuse.
|
||||
pub fn register<F, R>(&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<R: ViewDelegate + 'static>(&self, identifier: &'static str) -> ListViewRow<R> {
|
||||
#[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<R> = 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<T> ListView<T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn insert_rows<I: IntoIterator<Item = usize>>(&self, indexes: I, animations: ListViewAnimation) {
|
||||
pub fn insert_rows<I: IntoIterator<Item = usize>>(&self, indexes: I, animation: RowAnimation) {
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe {
|
||||
let index_set: id = msg_send![class!(NSMutableIndexSet), new];
|
||||
|
@ -306,12 +406,12 @@ impl<T> ListView<T> {
|
|||
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<T> ListView<T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn remove_rows<I: IntoIterator<Item = usize>>(&self, indexes: I, animations: ListViewAnimation) {
|
||||
pub fn remove_rows<I: IntoIterator<Item = usize>>(&self, indexes: I, animations: RowAnimation) {
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe {
|
||||
let index_set: id = msg_send![class!(NSMutableIndexSet), new];
|
||||
|
@ -348,7 +448,7 @@ impl<T> ListView<T> {
|
|||
// 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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<T: ViewDelegate>(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<T: ViewDelegate>(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<T: ViewDelegate>() -> *c
|
|||
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, _, _));
|
||||
|
||||
// Cleanup
|
||||
decl.add_method(sel!(dealloc), dealloc::<T> as extern fn (&Object, _));
|
||||
|
||||
VIEW_CLASS = decl.register();
|
||||
});
|
||||
|
||||
|
|
|
@ -141,11 +141,49 @@ impl ListViewRow {
|
|||
}
|
||||
|
||||
impl<T> ListViewRow<T> 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<T> 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<T> {
|
||||
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::<R>(&*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<T> {
|
||||
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<T> {
|
||||
let mut delegate = Box::new(delegate);
|
||||
|
||||
pub fn with_boxed(mut delegate: Box<T>) -> ListViewRow<T> {
|
||||
let view = allocate_view(register_listview_row_class_with_delegate::<T>);
|
||||
unsafe {
|
||||
//let view: id = msg_send![register_view_class_with_delegate::<T>(), new];
|
||||
|
@ -171,13 +209,35 @@ impl<T> ListViewRow<T> 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);
|
||||
}
|
||||
|
||||
impl<T> From<&ListViewRow<T>> for ShareId<Object> {
|
||||
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<T> From<&ListViewRow<T>> for ShareId<Object> {
|
||||
fn from(row: &ListViewRow<T>) -> ShareId<Object> {
|
||||
row.objc.clone()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
impl<T> ListViewRow<T> {
|
||||
/// An internal method that returns a clone of this object, sans references to the delegate or
|
||||
|
@ -199,6 +259,15 @@ impl<T> ListViewRow<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
|
|
@ -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<RowAction> { Vec::new() }
|
||||
|
||||
/// Called when this is about to be added to the view heirarchy.
|
||||
fn will_appear(&self, _animated: bool) {}
|
||||
|
|
|
@ -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<Vec<PathBuf>, Box<dyn std::error::Error>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<F>(&self, url: &Url, config: ThumbnailConfig, callback: F)
|
||||
pub fn generate<F>(&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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue