diff --git a/src/button.rs b/src/button/mod.rs similarity index 90% rename from src/button.rs rename to src/button/mod.rs index 953fd70..7ec6fcc 100644 --- a/src/button.rs +++ b/src/button/mod.rs @@ -24,8 +24,12 @@ use crate::macos::FocusRingType; /// where our `NSButton` lives. #[derive(Debug)] pub struct Button { + /// A handle for the underlying Objective-C object. pub objc: ShareId, + + /// A reference to an image, if set. We keep a copy to avoid any ownership snafus. pub image: Option, + handler: Option, /// A pointer to the Objective-C runtime top layout constraint. @@ -60,7 +64,11 @@ impl Button { let title = NSString::new(text); let view: id = unsafe { - let button: id = msg_send![register_class(), buttonWithTitle:&*title target:nil action:nil]; + let button: id = msg_send![register_class(), buttonWithTitle:&*title + target:nil + action:nil + ]; + let _: () = msg_send![button, setWantsLayer:YES]; let _: () = msg_send![button, setTranslatesAutoresizingMaskIntoConstraints:NO]; button @@ -81,6 +89,7 @@ impl Button { } } + /// Sets an image on the underlying button. pub fn set_image(&mut self, image: Image) { unsafe { let _: () = msg_send![&*self.objc, setImage:&*image.0]; @@ -117,6 +126,8 @@ impl Button { } } + /// Set a key to be bound to this button. When the key is pressed, the action coupled to this + /// button will fire. pub fn set_key_equivalent(&self, key: &str) { let key = NSString::new(key); @@ -241,19 +252,47 @@ fn register_class() -> *const Class { #[cfg(feature = "macos")] #[derive(Debug)] pub enum BezelStyle { + /// A standard circular button. Circular, + + /// A standard disclosure style button. Disclosure, + + /// The standard looking "Help" (?) button. HelpButton, + + /// An inline style, varies across OS's. Inline, + + /// A recessed style, varies slightly across OS's. Recessed, + + /// A regular square style, with no special styling. RegularSquare, + + /// A standard rounded rectangle. RoundRect, + + /// A standard rounded button. Rounded, + + /// A standard rounded disclosure button. RoundedDisclosure, + + /// A shadowless square styl.e ShadowlessSquare, + + /// A small square style. SmallSquare, + + /// A textured rounded style. TexturedRounded, + + /// A textured square style. TexturedSquare, + + /// Any style that's not known by this framework (e.g, if Apple + /// introduces something new). Unknown(NSUInteger) } diff --git a/src/events.rs b/src/events.rs index ff9a8aa..a4f3cc4 100644 --- a/src/events.rs +++ b/src/events.rs @@ -3,12 +3,22 @@ use crate::foundation::NSUInteger; +/// Flags that indicate a key is in the mix for an event. #[derive(Clone, Copy, Debug)] pub enum EventModifierFlag { + /// CapsLock (or shift... oddly named...) is held. CapsLock, + + /// Control is held. Control, + + /// Option is held. Option, + + /// Command (CMD) is held. Command, + + /// Device independent flags mask. DeviceIndependentFlagsMask } @@ -36,7 +46,9 @@ impl From<&EventModifierFlag> for NSUInteger { } } +/// Represents an event type that you can request to be notified about. #[derive(Clone, Copy, Debug)] pub enum EventType { + /// A keydown event. KeyDown } diff --git a/src/filesystem/enums.rs b/src/filesystem/enums.rs index 90c2653..1a464a2 100644 --- a/src/filesystem/enums.rs +++ b/src/filesystem/enums.rs @@ -2,15 +2,31 @@ use crate::foundation::{NSInteger, NSUInteger}; +/// Represents a modal response for macOS modal dialogs. #[derive(Copy, Clone, Debug)] pub enum ModalResponse { + /// The user hit the "Ok" button. Ok, + + /// Continue. Continue, + + /// Canceled. Canceled, + + /// Stopped. Stopped, + + /// Aborted. Aborted, + + /// The first button in the dialog was clicked. FirstButtonReturned, + + /// The second button in the dialog was clicked. SecondButtonReturned, + + /// The third button in the dialog was clicked. ThirdButtonReturned } @@ -34,12 +50,22 @@ impl From for ModalResponse { } } +/// Represents a type of search path used in file manager calls. #[derive(Copy, Clone, Debug)] pub enum SearchPathDomainMask { + /// User files and folders. User, + + /// Local volume files and folders. Local, + + /// Netowrk files and folders. Network, + + /// Search all domains. Not typically used these days. Domain, + + /// Search all domains. Not typically used these days. AllDomains } @@ -55,34 +81,95 @@ impl From for NSUInteger { } } +/// Represents a type of search path to use. +/// +/// This enum is particularly useful for applications that need to exist both inside and outside of +/// the sandbox. For example: `SearchPathDirectory::Documents` will find the standard `Documents` +/// directory outside of the sandbox, but use the sandbox `Documents` directory in sandboxed +/// applications. #[derive(Copy, Clone, Debug)] pub enum SearchPathDirectory { + /// The applications folder. Applications, + + /// Unsupported applications and demo versions. Not generally used these days. DemoApplications, + + /// Developer applications (_/Developer/Applications_). Not generally used these days. DeveloperApplications, + + /// System and network admin apps. AdminApplications, + + /// User-visible docs, support, and config files. Library, + + /// Dev resources. (_/Developer_) Developer, + + /// User home directories. (_/Users_) User, + + /// Documentation. Documentation, + + /// Documents directory. Documents, + + /// Core Services (_/System/Library/CoreServices_) CoreServices, + + /// User's autosaved documents (_/Library/Autosave Information_) AutosavedInformation, + + /// The current user's Desktop directory. Desktop, + + /// Discardable cache files. (_/Library/Caches_) Caches, + + /// App support files (_/Library/Application Support_) ApplicationSupport, + + /// The curent user's Downloads directory. Downloads, + + /// Input methods (_/Library/Input Methods_) InputMethods, + + /// The current user's Movies directory. Movies, + + /// The current user's Music directory. Music, + + /// The current user's pictures directory. Pictures, + + /// System PPD files (_/Library/Printers/PPDs_) PrinterDescription, + + /// The current user's public sharing directory. SharedPublic, + + /// The Preferences Pane directory, where system preferences files live. + /// (_/Library/PreferencePanes_) PreferencePanes, + + /// The user scripts folder for the calling application + /// (_~/Library/Application Scripts/_). ApplicationScripts, + + /// Constant used in creating a temp directory. ItemReplacement, + + /// All directories where apps can be stored. AllApplications, + + /// All directories where resources can be stored. AllLibraries, + + /// The Trash directory. Trash } @@ -112,7 +199,6 @@ impl From for NSUInteger { SearchPathDirectory::SharedPublic => 21, SearchPathDirectory::PreferencePanes => 22, SearchPathDirectory::ApplicationScripts => 23, - SearchPathDirectory::ItemReplacement => 99, SearchPathDirectory::AllApplications => 100, SearchPathDirectory::AllLibraries => 101, diff --git a/src/filesystem/manager.rs b/src/filesystem/manager.rs index 8576c5a..6d30ebb 100644 --- a/src/filesystem/manager.rs +++ b/src/filesystem/manager.rs @@ -2,7 +2,7 @@ //! tricky, and this transparently handles it for you). use std::error::Error; -use std::sync::RwLock; +use std::sync::{Arc, RwLock}; use objc_id::Id; use objc::runtime::{BOOL, Object}; @@ -10,47 +10,51 @@ use objc::{class, msg_send, sel, sel_impl}; use url::Url; use crate::foundation::{id, nil, NO, NSString, NSUInteger}; -use crate::error::{Error as AppKitError}; +use crate::error::Error as AppKitError; use crate::filesystem::enums::{SearchPathDirectory, SearchPathDomainMask}; -#[derive(Debug)] -pub struct FileManager { - pub manager: RwLock> -} +/// A FileManager can be used for file operations (moving files, etc). +/// +/// If your app is not sandboxed, you can use your favorite Rust library - +/// but if you _are_ operating in the sandbox, there's a good chance you'll want to use this. +/// +/// @TODO: Couldn't this just be a ShareId? +#[derive(Clone, Debug)] +pub struct FileManager(pub Arc>>); 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 { - FileManager { - manager: RwLock::new(unsafe { - let manager: id = msg_send![class!(NSFileManager), defaultManager]; - Id::from_ptr(manager) - }) - } + FileManager(Arc::new(RwLock::new(unsafe { + let manager: id = msg_send![class!(NSFileManager), defaultManager]; + Id::from_ptr(manager) + }))) } } 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) - }) - } + FileManager(Arc::new(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> { + pub fn get_directory( + &self, + directory: SearchPathDirectory, + in_domain: SearchPathDomainMask + ) -> Result> { let dir: NSUInteger = directory.into(); let mask: NSUInteger = in_domain.into(); let directory = unsafe { - let manager = self.manager.read().unwrap(); + let manager = self.0.read().unwrap(); let dir: id = msg_send![&**manager, URLForDirectory:dir inDomain:mask appropriateForURL:nil @@ -76,7 +80,7 @@ impl FileManager { // 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 manager = self.0.read().unwrap(); let error: id = nil; let result: BOOL = msg_send![&**manager, moveItemAtURL:from_url toURL:to_url error:&error]; diff --git a/src/filesystem/save.rs b/src/filesystem/save.rs index 601f7fc..267f495 100644 --- a/src/filesystem/save.rs +++ b/src/filesystem/save.rs @@ -47,12 +47,23 @@ impl FileSavePanel { } } + /// @TODO: Do we even need this? pub fn set_delegate(&mut self) {} - pub fn set_suggested_filename(&mut self, suggested_filename: &str) { + /// Sets a suggested filename for the save dialog. The user can still change this if they + /// choose to, but it's generally best practice to call this. + pub fn set_suggested_filename>(&mut self, suggested_filename: S) { unsafe { - let filename = NSString::new(suggested_filename); - let _: () = msg_send![&*self.panel, setNameFieldStringValue:filename]; + let filename = NSString::new(suggested_filename.as_ref()); + let _: () = msg_send![&*self.panel, setNameFieldStringValue:&*filename]; + } + } + + /// Set the message text displayed in the panel. + pub fn set_message>(&mut self, message: S) { + unsafe { + let message = NSString::new(message.as_ref()); + let _: () = msg_send![&*self.panel, setMessage:&*message]; } } diff --git a/src/filesystem/select.rs b/src/filesystem/select.rs index 66dd9e6..f6a7c35 100644 --- a/src/filesystem/select.rs +++ b/src/filesystem/select.rs @@ -82,6 +82,14 @@ impl FileSelectPanel { self.can_choose_files = can_choose; } + /// Set the message text displayed in the panel. + pub fn set_message>(&mut self, message: S) { + unsafe { + let message = NSString::new(message.as_ref()); + let _: () = msg_send![&*self.panel, setMessage:&*message]; + } + } + /// Sets whether the user can choose directories. pub fn set_can_choose_directories(&mut self, can_choose: bool) { unsafe { diff --git a/src/foundation/string.rs b/src/foundation/string.rs index 101e43c..8cfbd5a 100644 --- a/src/foundation/string.rs +++ b/src/foundation/string.rs @@ -17,6 +17,7 @@ const UTF8_ENCODING: usize = 4; /// side is fairly battle tested. #[derive(Debug)] pub struct NSString<'a> { + /// A reference to the backing `NSString`. pub objc: Id, phantom: PhantomData<&'a ()> } diff --git a/src/image/image.rs b/src/image/image.rs index 5cc2933..2e8d00f 100644 --- a/src/image/image.rs +++ b/src/image/image.rs @@ -15,11 +15,19 @@ use crate::foundation::{id, YES, NO, NSString}; use crate::utils::os; use super::icons::*; -#[derive(Debug)] +/// Specifies resizing behavior for image drawing. +#[derive(Copy, Clone, Debug)] pub enum ResizeBehavior { + /// Fit to the aspect ratio. AspectFit, + + /// Fill the aspect ratio. AspectFill, + + /// Stretch as necessary. Stretch, + + /// Center and then let whatever else flow around it. Center } @@ -42,6 +50,8 @@ fn min_cgfloat(x: CGFloat, y: CGFloat) -> CGFloat { } impl ResizeBehavior { + /// Given a source and target rectangle, configures and returns a new rectangle configured with + /// the resizing properties of this enum. pub fn apply(&self, source: CGRect, target: CGRect) -> CGRect { // if equal, just return source if @@ -94,13 +104,22 @@ impl ResizeBehavior { } } -#[derive(Debug)] +/// A config object that specifies how drawing into an image context should scale. +#[derive(Copy, Clone, Debug)] pub struct DrawConfig { + /// The size of the source. pub source: (CGFloat, CGFloat), + + /// The size of the target. This may be the same as the source; if not, the source will be + /// scaled to this size. pub target: (CGFloat, CGFloat), + + /// The type of resizing to use during drawing and scaling. pub resize: ResizeBehavior } +/// Wraps `NSImage` on macOS, and `UIImage` on iOS and tvOS. Can be used to display images, icons, +/// and so on. #[derive(Clone, Debug)] pub struct Image(pub ShareId); @@ -195,7 +214,11 @@ impl Image { let block = block.copy(); Image(unsafe { - let img: id = msg_send![class!(NSImage), imageWithSize:target_frame.size flipped:YES drawingHandler:block]; + let img: id = msg_send![class!(NSImage), imageWithSize:target_frame.size + flipped:YES + drawingHandler:block + ]; + ShareId::from_ptr(img) }) } diff --git a/src/input/mod.rs b/src/input/mod.rs index 9cc5961..284da77 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -223,6 +223,7 @@ impl TextField { } } + /// The the text alignment style for this control. pub fn set_text_alignment(&self, alignment: TextAlign) { unsafe { let alignment: NSInteger = alignment.into(); @@ -232,7 +233,7 @@ impl TextField { /// Sets the font for this input. pub fn set_font>(&self, font: F) { - let font = font.as_ref(); + let font = font.as_ref().clone(); unsafe { let _: () = msg_send![&*self.objc, setFont:&*font]; diff --git a/src/layout/attributes.rs b/src/layout/attributes.rs index d37360a..2f1e657 100644 --- a/src/layout/attributes.rs +++ b/src/layout/attributes.rs @@ -208,7 +208,12 @@ impl From for LayoutFormat { /// Specifies layout priority. #[derive(Debug)] pub enum LayoutPriority { + /// Highest priority. Required, + + /// High priority. Will bend if absolutely necessary. High, + + /// Low priority. Low } diff --git a/src/listview/enums.rs b/src/listview/enums.rs index 7dedc24..3d21702 100644 --- a/src/listview/enums.rs +++ b/src/listview/enums.rs @@ -42,9 +42,15 @@ impl Into for RowAnimation { } } +/// Specifies a row edge. +/// +/// Generally used to indicate where row actions (swipe-to-reveal) should appear. #[derive(Copy, Clone, Debug)] pub enum RowEdge { + /// The leading edge. Leading, + + /// The trailing edge. Trailing } diff --git a/src/listview/macos.rs b/src/listview/macos.rs index a5f1674..fa3749e 100644 --- a/src/listview/macos.rs +++ b/src/listview/macos.rs @@ -18,7 +18,7 @@ use crate::macos::menu::{Menu, MenuItem}; use crate::foundation::{load_or_register_class, id, nil, YES, NO, NSArray, NSInteger, NSUInteger}; use crate::dragdrop::DragInfo; use crate::listview::{ - LISTVIEW_DELEGATE_PTR, LISTVIEW_CELL_VENDOR_PTR, + LISTVIEW_DELEGATE_PTR, ListViewDelegate, RowEdge }; use crate::utils::load; @@ -197,7 +197,6 @@ pub(crate) fn register_listview_class() -> *const Class { pub(crate) fn register_listview_class_with_delegate(instance: &T) -> *const Class { load_or_register_class("NSTableView", instance.subclass_name(), |decl| unsafe { 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); diff --git a/src/listview/mod.rs b/src/listview/mod.rs index 393af40..46a55b9 100644 --- a/src/listview/mod.rs +++ b/src/listview/mod.rs @@ -53,7 +53,8 @@ use crate::color::Color; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::pasteboard::PasteboardType; use crate::scrollview::ScrollView; -use crate::utils::CGSize; +use crate::utils::{os, CellFactory, CGSize}; +use crate::view::ViewDelegate; #[cfg(target_os = "macos")] use crate::macos::menu::MenuItem; @@ -83,7 +84,6 @@ 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}; @@ -91,57 +91,6 @@ 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 common_init(class: *const Class) -> id { unsafe { @@ -232,7 +181,7 @@ pub struct ListView { /// allocation and reuse, which is necessary for an "infinite" listview. cell_factory: CellFactory, - pub menu: PropertyNullable>, + menu: PropertyNullable>, /// A pointer to the Objective-C runtime view controller. pub objc: ShareId, @@ -277,7 +226,7 @@ impl Default for ListView { } impl ListView { - /// Returns a default `View`, suitable for + /// @TODO: The hell is this for? pub fn new() -> Self { let class = register_listview_class(); let view = common_init(class); @@ -333,9 +282,7 @@ impl ListView where T: ListViewDelegate + 'static { //let view: id = msg_send![register_view_class_with_delegate::(), new]; //let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; 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]; }; @@ -446,13 +393,23 @@ impl ListView { } } - /// Style + /// Sets the style for the underlying NSTableView. This property is only supported on macOS + /// 11.0+, and will always be `FullWidth` on anything older. + #[cfg(feature = "macos")] pub fn set_style(&self, style: crate::foundation::NSInteger) { - unsafe { - let _: () = msg_send![&*self.objc, setStyle:style]; + if os::is_minimum_version(11) { + unsafe { + let _: () = msg_send![&*self.objc, setStyle:style]; + } } } + /// Set whether this control can appear with no row selected. + /// + /// This defaults to `true`, but some macOS pieces (e.g, a sidebar) may want this set to + /// `false`. This can be particularly useful when implementing a Source List style sidebar + /// view for navigation purposes. + #[cfg(feature = "macos")] pub fn set_allows_empty_selection(&self, allows: bool) { unsafe { let _: () = msg_send![&*self.objc, setAllowsEmptySelection:match allows { @@ -462,12 +419,14 @@ impl ListView { } } + /// Set the selection highlight style. pub fn set_selection_highlight_style(&self, style: crate::foundation::NSInteger) { unsafe { let _: () = msg_send![&*self.objc, setSelectionHighlightStyle:style]; } } + /// Select the rows at the specified indexes, optionally adding to any existing selections. pub fn select_row_indexes(&self, indexes: &[usize], extends_existing: bool) { unsafe { let index_set: id = msg_send![class!(NSMutableIndexSet), new]; @@ -483,6 +442,16 @@ impl ListView { } } + /// This method should be used when inserting or removing multiple rows at once. Under the + /// hood, it batches the changes and tries to ensure things are done properly. The provided + /// `ListView` for the handler is your `ListView`, and you can call `insert_rows`, + /// `reload_rows`, or `remove_rows` from there. + /// + /// ```rust,no_run + /// list_view.perform_batch_updates(|listview| { + /// listview.insert_rows(&[0, 2], RowAnimation::SlideDown); + /// }); + /// ``` pub fn perform_batch_updates(&self, update: F) { #[cfg(target_os = "macos")] unsafe { @@ -495,6 +464,11 @@ impl ListView { } } + /// Insert new rows at the specified indexes, with the specified animation. + /// + /// Your underlying data store must be updated *before* calling this. If inserting multiple + /// rows at once, you should also run this inside a `perform_batch_updates` call, as that will + /// optimize things accordingly. pub fn insert_rows(&self, indexes: &[usize], animation: RowAnimation) { #[cfg(target_os = "macos")] unsafe { @@ -514,6 +488,7 @@ impl ListView { } } + /// Reload the rows at the specified indexes. pub fn reload_rows(&self, indexes: &[usize]) { #[cfg(target_os = "macos")] unsafe { @@ -532,6 +507,11 @@ impl ListView { } } + /// Remove rows at the specified indexes, with the specified animation. + /// + /// Your underlying data store must be updated *before* calling this. If removing multiple + /// rows at once, you should also run this inside a `perform_batch_updates` call, as that will + /// optimize things accordingly. pub fn remove_rows(&self, indexes: &[usize], animations: RowAnimation) { #[cfg(target_os = "macos")] unsafe { @@ -612,16 +592,43 @@ impl ListView { } } + /// Reloads the underlying ListView. This is more expensive than handling insert/reload/remove + /// calls yourself, but often easier to implement. + /// + /// Calling this will reload (and redraw) your listview based on whatever the data source + /// reports back. pub fn reload(&self) { unsafe { let _: () = msg_send![&*self.objc, reloadData]; } } + /// Returns the selected row. pub fn get_selected_row_index(&self) -> NSInteger { unsafe { msg_send![&*self.objc, selectedRow] } } + /// Returns the currently clicked row. This is macOS-specific, and is generally used in context + /// menu generation to determine what item the context menu should be for. If the clicked area + /// is not an actual row, this will return `-1`. + /// + /// For example (minus the other necessary ListViewDelegate pieces): + /// + /// ```rust,no_run + /// impl ListViewDelegate for MyListView { + /// fn context_menu(&self) -> Vec { + /// let clicked_row = self.list_view.get_clicked_row_index(); + /// + /// // You could treat this as a "new" menu. + /// if clicked_row == -1 { + /// return vec![]; + /// } + /// + /// // User right-clicked on a row, so let's show an edit menu. + /// vec![MenuItem::new("Edit")] + /// } + /// } + /// ``` pub fn get_clicked_row_index(&self) -> NSInteger { unsafe { msg_send![&*self.objc, clickedRow] } } diff --git a/src/macos/app/mod.rs b/src/macos/app/mod.rs index 87dab08..001d7a5 100644 --- a/src/macos/app/mod.rs +++ b/src/macos/app/mod.rs @@ -83,10 +83,19 @@ fn shared_application(handler: F) { /// implement the `Dispatcher` trait to receive messages that you might dispatch from deeper in the /// application. pub struct App { + /// The underlying Objective-C Object. pub objc: Id, + + /// The underlying Objective-C Object, which in this case is a delegate that forwards to the + /// app delegate. pub objc_delegate: Id, + + /// The stored `AppDelegate`. pub delegate: Box, + + /// The main-thread AutoReleasePool. Drains on app exit. pub pool: AutoReleasePool, + _message: std::marker::PhantomData } diff --git a/src/macos/cursor.rs b/src/macos/cursor.rs index ee5ab86..a8b0be3 100644 --- a/src/macos/cursor.rs +++ b/src/macos/cursor.rs @@ -1,34 +1,91 @@ - use objc::{class, msg_send, sel, sel_impl}; -use crate::foundation::id; +use crate::foundation::{id, YES, NO}; + +/// Represents a type of cursor that you can associate with mouse movement. +/// @TODO: Loading? #[derive(Debug)] pub enum CursorType { + /// A standard arrow. Arrow, + + /// A crosshair. Crosshair, + + /// A closed hand, typically for mousedown and drag. ClosedHand, + + /// An open hand, typically for indicating draggable. OpenHand, + + /// A pointing hand, like clicking a link. PointingHand, + /// Indicator that something can be resized to the left. ResizeLeft, + + /// Indicator that something can be resized to the right. ResizeRight, + + /// Indicator that something can be resized on the horizontal axis. ResizeLeftRight, + /// Indicates that something can be resized up. ResizeUp, + + /// Indicates that something can be resized down. ResizeDown, + + /// Indicator that something can be resized on the vertical axis. ResizeUpDown, + /// Otherwise known as the "poof" or "cloud" cursor. Indicates something will vanish, like + /// dragging into the Trash. DisappearingItem, + /// Indicate an insertion point, like for text. IBeam, + + /// The vertical version of `CursorType::IBeam`. IBeamVertical, + /// Indicates an operation is illegal. OperationNotAllowed, + + /// The drag link cursor. DragLink, + + /// Used for drag-and-drop usually, will displayu the standard "+" icon next to the cursor. DragCopy, + + /// Indicates a context menu will open. ContextMenu } +/// A wrapper around NSCursor. +/// +/// You use then when you need to control how the cursor (pointer) should appear. Like `NSCursor`, +/// this is stack based - you push, and you pop. You are responsible for ensuring that this is +/// correctly popped! +/// +/// For a very abbreviated example: +/// +/// ```rust,no_run +/// impl ViewDelegate for MyView { +/// fn dragging_entered(&self, _info: DragInfo) -> DragOperation { +/// Cursor::push(CursorType::DragCopy); +/// DragOperation::Copy +/// } +/// +/// fn dragging_exited(&self, _info: DragInfo) { +/// Cursor::pop(); +/// } +/// } +/// ``` +/// +/// This will show the "add files +" indicator when the user has entered the dragging threshold +/// with some items that trigger it, and undo the cursor when the user leaves (regardless of drop +/// status). #[derive(Debug)] pub struct Cursor; @@ -82,4 +139,19 @@ impl Cursor { let _: () = msg_send![class!(NSCursor), unhide]; } } + + /// Sets the cursor to hidden, but will reveal it if the user moves the mouse. + /// + /// Potentially useful for games and other immersive experiences. + /// + /// If you use this, do _not_ use `unhide` - just call this with the inverted boolean value. + /// Trying to invert this with `unhide` will result in undefined system behavior. + pub fn set_hidden_until_mouse_moves(status: bool) { + unsafe { + let _: () = msg_send![class!(NSCursor), setHiddenUntilMouseMoves:match status { + true => YES, + false => NO + }]; + } + } } diff --git a/src/networking/mod.rs b/src/networking/mod.rs index b17087f..854055f 100644 --- a/src/networking/mod.rs +++ b/src/networking/mod.rs @@ -1,28 +1,54 @@ //! A lightweight wrapper over some networking components, like `NSURLRequest` and co. -//! This is currently not meant to be exhaustive. +//! +/// At the moment, this is mostly used for inspection of objects returned from system +/// calls, as `NSURL` is pervasive in some filesystem references. Over time this may grow to +/// include a proper networking stack, but the expectation for v0.1 is that most apps will want to +/// use their standard Rust networking libraries (however... odd... the async story may be). use objc::{msg_send, sel, sel_impl}; use objc::runtime::Object; -use objc_id::Id; +use objc_id::ShareId; use crate::foundation::{id, NSString}; +/// A wrapper around `NSURLRequest`. #[derive(Debug)] -pub struct URLRequest { - pub inner: Id -} +pub struct URLRequest(ShareId); impl URLRequest { - pub fn with(inner: id) -> Self { - URLRequest { - inner: unsafe { Id::from_ptr(inner) } - } + /// Wraps and retains an `NSURLRequest`. + pub fn with(request: id) -> Self { + URLRequest(unsafe { + ShareId::from_ptr(request) + }) } - pub fn url(&self) -> String { + /// Returns the underlying request URL as an owned `String`. + pub fn absolute_url(&self) -> String { NSString::from_retained(unsafe { - let url: id = msg_send![&*self.inner, URL]; + let url: id = msg_send![&*self.0, URL]; msg_send![url, absoluteString] }).to_string() } } + +#[cfg(test)] +mod tests { + use objc::{class, msg_send, sel, sel_impl}; + + use crate::foundation::{id, NSString}; + use crate::networking::URLRequest; + + #[test] + fn test_urlrequest() { + let endpoint = "https://rymc.io/"; + + let url = unsafe { + let url = NSString::new(endpoint); + let url: id = msg_send![class!(NSURL), URLWithString:&*url]; + URLRequest::with(msg_send![class!(NSURLRequest), requestWithURL:url]) + }; + + assert_eq!(&url.absolute_url(), endpoint); + } +} diff --git a/src/notification_center/traits.rs b/src/notification_center/traits.rs index 21d1c0c..916e8c3 100644 --- a/src/notification_center/traits.rs +++ b/src/notification_center/traits.rs @@ -1,7 +1,23 @@ +/// A trait for handling dispatched messages on the AppDelegate. +/// +/// You can use this for a jank message dispatching mechanism. It has no guarantees concerning +/// performance, but is good enough for many applications. Implement this trait on your struct +/// that implements `AppDelegate`, and then dispatch messages like the following: +/// +/// ```rust,no_run +/// App::::dispatch_main(your_message); +/// ``` +/// +/// This will asynchronously loop a message back to the "top" of your app, via your app delegate. +/// You can process it from there. pub trait Dispatcher { + /// The type of Message you're sending. This should be lightweight and thread safe. type Message: Send + Sync; + /// Called when a message is looped back on the _main_ queue. This is where all UI work should + /// be happening. fn on_ui_message(&self, _message: Self::Message) {} + /// Called when a message is looped back on a background queue. fn on_background_message(&self, _message: Self::Message) {} } diff --git a/src/progress/macos.rs b/src/progress/macos.rs deleted file mode 100644 index 8c5dec3..0000000 --- a/src/progress/macos.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::sync::Once; - -use objc::declare::ClassDecl; -use objc::runtime::{Class, Object, Sel, BOOL}; -use objc::{class, sel, sel_impl}; - -/// 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. -pub(crate) fn register_progress_indicator_class() -> *const Class { - static mut VIEW_CLASS: *const Class = 0 as *const Class; - static INIT: Once = Once::new(); - - INIT.call_once(|| unsafe { - let superclass = class!(NSProgressIndicator); - let decl = ClassDecl::new("RSTProgressIndicator", superclass).unwrap(); - VIEW_CLASS = decl.register(); - }); - - unsafe { VIEW_CLASS } -} diff --git a/src/progress/mod.rs b/src/progress/mod.rs index 3534f18..06def78 100644 --- a/src/progress/mod.rs +++ b/src/progress/mod.rs @@ -1,53 +1,63 @@ +//! A progress indicator widget. +//! +//! This control wraps `NSProgressIndicator` on macOS, and +//! `UIProgressView+UIActivityIndicatorView` on iOS and tvOS. It operates in two modes: determinate +//! (where you have a fixed start and end) and indeterminate (infinite; it will go and go until you +//! tell it to stop). +//! +//! ```rust,no_run +//! let indicator = ProgressIndicator::new(); +//! indicator.set_indeterminate(true); +//! my_view.add_subview(&indicator); +//! ``` + +use core_graphics::base::CGFloat; + use objc_id::ShareId; use objc::runtime::{Class, Object}; -use objc::{msg_send, sel, sel_impl}; +use objc::{class, msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSUInteger}; use crate::color::Color; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; -#[cfg(target_os = "macos")] -mod macos; - -#[cfg(target_os = "macos")] -use macos::{register_progress_indicator_class}; - #[cfg(target_os = "ios")] mod ios; #[cfg(target_os = "ios")] -use ios::{register_progress_indicator_class}; +use ios::register_progress_indicator_class; mod enums; pub use enums::ProgressIndicatorStyle; +/// A control used for reporting progress to a user visually. #[derive(Debug)] pub struct ProgressIndicator { - /// A pointer to the Objective-C runtime view controller. + /// A pointer to the Objective-C Object. pub objc: ShareId, - /// A pointer to the Objective-C runtime top layout constraint. + /// A pointer to the Objective-C top layout constraint. pub top: LayoutAnchorY, - /// A pointer to the Objective-C runtime leading layout constraint. + /// A pointer to the Objective-C leading layout constraint. pub leading: LayoutAnchorX, - /// A pointer to the Objective-C runtime trailing layout constraint. + /// A pointer to the Objective-C trailing layout constraint. pub trailing: LayoutAnchorX, - /// A pointer to the Objective-C runtime bottom layout constraint. + /// A pointer to the Objective-C bottom layout constraint. pub bottom: LayoutAnchorY, - /// A pointer to the Objective-C runtime width layout constraint. + /// A pointer to the Objective-C width layout constraint. pub width: LayoutAnchorDimension, - /// A pointer to the Objective-C runtime height layout constraint. + /// A pointer to the Objective-C height layout constraint. pub height: LayoutAnchorDimension, - /// A pointer to the Objective-C runtime center X layout constraint. + /// A pointer to the Objective-C center X layout constraint. pub center_x: LayoutAnchorX, - /// A pointer to the Objective-C runtime center Y layout constraint. + /// A pointer to the Objective-C center Y layout constraint. pub center_y: LayoutAnchorY } @@ -58,13 +68,15 @@ impl Default for ProgressIndicator { } impl ProgressIndicator { - /// Returns a default `ProgressIndicator`, suitable for + /// Returns a default `ProgressIndicator`. You should retain this yourself for as long as you + /// need it to stay around. pub fn new() -> Self { let view = unsafe { - let view: id = msg_send![register_progress_indicator_class(), new]; + #[cfg(feature = "macos")] + let view: id = msg_send![class!(NSProgressIndicator), new]; let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; - #[cfg(target_os = "macos")] + #[cfg(feature = "macos")] let _: () = msg_send![view, setWantsLayer:YES]; view @@ -85,17 +97,6 @@ impl ProgressIndicator { } impl ProgressIndicator { - // 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(); - - unsafe { - let cg: id = msg_send![bg, CGColor]; - let layer: id = msg_send![&*self.objc, layer]; - let _: () = msg_send![layer, setBackgroundColor:cg]; - } - }*/ - /// Starts the animation for an indeterminate indicator. pub fn start_animation(&self) { unsafe { @@ -103,18 +104,22 @@ impl ProgressIndicator { } } + /// Stops any animations that are currently happening on this indicator (e.g, if it's an + /// indeterminate looping animation). pub fn stop_animation(&self) { unsafe { let _: () = msg_send![&*self.objc, stopAnimation:nil]; } } - pub fn increment(&self, by: f64) { + /// Increment the progress indicator by the amount specified. + pub fn increment(&self, amount: f64) { unsafe { - let _: () = msg_send![&*self.objc, incrementBy:by]; + let _: () = msg_send![&*self.objc, incrementBy:amount]; } } + /// Set the style for the progress indicator. pub fn set_style(&self, style: ProgressIndicatorStyle) { unsafe { let style = style as NSUInteger; @@ -122,6 +127,10 @@ impl ProgressIndicator { } } + /// Set whether this is an indeterminate indicator or not. Indeterminate indicators are + /// "infinite" and their appearance is that of a circular spinner. + /// + /// Invert this to go back to a bar appearance. pub fn set_indeterminate(&self, is_indeterminate: bool) { unsafe { let _: () = msg_send![&*self.objc, setIndeterminate:match is_indeterminate { @@ -130,6 +139,27 @@ impl ProgressIndicator { }]; } } + + /// Sets the value of this progress indicator. + /// + /// If this progress indicator is indeterminate, this will have no effect. + pub fn set_value(&self, value: f64) { + let value = value as CGFloat; + + unsafe { + let _: () = msg_send![&*self.objc, setDoubleValue:value]; + } + } + + /// Set whether this control is hidden or not. + pub fn set_hidden(&self, hidden: bool) { + unsafe { + let _: () = msg_send![&*self.objc, setHidden:match hidden { + true => YES, + false => NO + }]; + } + } } impl Layout for ProgressIndicator { @@ -147,20 +177,19 @@ impl Layout for ProgressIndicator { } impl Drop for ProgressIndicator { - /// A bit of extra cleanup for delegate callback pointers. If the originating `ProgressIndicator` is being + /// A bit of extra cleanup for delegate callback pointers. + /// If the originating `ProgressIndicator` is being /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if - /// this has a superview (i.e, it's in the heirarchy) on the AppKit side. If it does, we go + /// this has a superview (i.e, it's in the heirarchy) on the Objective-C side. If it does, we go /// ahead and remove it - this is intended to match the semantics of how Rust handles things). /// /// There are, thankfully, no delegates we need to break here. fn drop(&mut self) { - /*if self.delegate.is_some() { - unsafe { - let superview: id = msg_send![&*self.objc, superview]; - if superview != nil { - let _: () = msg_send![&*self.objc, removeFromSuperview]; - } + unsafe { + let superview: id = msg_send![&*self.objc, superview]; + if superview != nil { + let _: () = msg_send![&*self.objc, removeFromSuperview]; } - }*/ + } } } diff --git a/src/scrollview/traits.rs b/src/scrollview/traits.rs index cbefe13..8c31612 100644 --- a/src/scrollview/traits.rs +++ b/src/scrollview/traits.rs @@ -1,6 +1,9 @@ use crate::dragdrop::{DragInfo, DragOperation}; use crate::scrollview::ScrollView; +/// A ScrollViewDelegate implements methods that you might need or want to respond to. In addition +/// to scroll-specific events, it enables implementing certain standard `View` handlers for things +/// like drag and drop. pub trait ScrollViewDelegate { /// Called when the View is ready to work with. You're passed a `View` - this is safe to /// store and use repeatedly, but it's not thread safe - any UI calls must be made from the diff --git a/src/switch.rs b/src/switch.rs index d22f0e1..8f8990c 100644 --- a/src/switch.rs +++ b/src/switch.rs @@ -18,6 +18,7 @@ use crate::utils::load; /// where our `NSSwitch` lives. #[derive(Debug)] pub struct Switch { + /// A pointer to the underlying Objective-C Object. pub objc: ShareId, handler: Option, diff --git a/src/text/label/mod.rs b/src/text/label/mod.rs index a40d3f1..e745ef6 100644 --- a/src/text/label/mod.rs +++ b/src/text/label/mod.rs @@ -223,7 +223,8 @@ impl Label { } /// Call this to set the text for the label. - pub fn set_text(&self, text: &str) { + pub fn set_text>(&self, text: S) { + let text = text.as_ref(); let s = NSString::new(text); unsafe { @@ -269,12 +270,20 @@ impl Label { } } + /// Sets the maximum number of lines. + pub fn set_max_number_of_lines(&self, num: NSInteger) { + unsafe { + let _: () = msg_send![&*self.objc, setMaximumNumberOfLines:num]; + } + } + /// Set the line break mode for this label. pub fn set_line_break_mode(&self, mode: LineBreakMode) { #[cfg(target_os = "macos")] unsafe { let cell: id = msg_send![&*self.objc, cell]; let mode = mode as NSUInteger; + let _: () = msg_send![cell, setTruncatesLastVisibleLine:YES]; let _: () = msg_send![cell, setLineBreakMode:mode]; } } diff --git a/src/utils/cell_factory.rs b/src/utils/cell_factory.rs new file mode 100644 index 0000000..8d6c4bd --- /dev/null +++ b/src/utils/cell_factory.rs @@ -0,0 +1,71 @@ +use std::any::Any; +use std::fmt; +use std::collections::HashMap; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::view::ViewDelegate; + +type CellFactoryMap = HashMap<&'static str, Box Box>>; + +/// A CellFactory is an struct that stores closures that instantiate view types. +/// +/// This is a pattern used in certain view types (e.g, `ListView`). This factory exists to enable +/// dynamic view registration and dequeueing. It stores a closure and erases the type to `Any`, and +/// supports querying for that time to get it back. +/// +/// It is explicitly designed to panic if it's unable to retrieve a stored item with the specified +/// type, as the views that use this would cease to function if the type can't be retrieved, and +/// it's better to blow up early. +#[derive(Clone)] +pub struct CellFactory(pub Rc>); + +impl CellFactory { + /// Creates and returns a new CellFactory. + pub fn new() -> Self { + CellFactory(Rc::new(RefCell::new(HashMap::new()))) + } + + /// Store a closure for the given identifier. + // @TODO: We might not need to do anything with this being `ViewDelegate`... + 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 + })); + } + + /// Attempts to retrieve the closure, downcasted to the specified type. This will panic if it's + /// unable to retrieve the closure with the requested type. + 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 for {}: 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); + } + } +} + +impl std::fmt::Debug for CellFactory { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CellFactory").finish() + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 21eac06..c9cdcef 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -12,6 +12,9 @@ use objc_id::ShareId; use crate::foundation::{id, BOOL, YES, NO}; +mod cell_factory; +pub use cell_factory::CellFactory; + pub mod os; /// A generic trait that's used throughout multiple different controls in this framework - acts as diff --git a/src/view/splitviewcontroller/mod.rs b/src/view/splitviewcontroller/mod.rs index a49beb2..d61f631 100644 --- a/src/view/splitviewcontroller/mod.rs +++ b/src/view/splitviewcontroller/mod.rs @@ -3,14 +3,22 @@ use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; use crate::foundation::{id, nil, NSString}; -use crate::layout::{Layout}; +use crate::layout::Layout; use crate::macos::toolbar::ToolbarItem; use crate::view::{View, ViewController, ViewDelegate}; -use crate::utils::Controller; +use crate::utils::{os, Controller}; +/// A SplitViewItem wraps a ViewController, and provides system hooks for operating in a +/// SplitView(Controller). +/// +/// This is typically created for you when you create a `SplitViewController`, but is exported in +/// case you need to hook into the underlying platform pieces. #[derive(Debug)] pub struct SplitViewItem { + /// The underlying Objective-C Object. pub objc: ShareId, + + /// The wrapped ViewController. pub view_controller: ViewController } @@ -18,65 +26,128 @@ impl SplitViewItem where T: ViewDelegate + 'static { + /// Creates and returns a new `SplitViewItem`. This has no special properties out the default + /// split view item type. pub fn item(view: T) -> Self { let view_controller = ViewController::new(view); - let objc = unsafe { - ShareId::from_ptr(msg_send![class!(NSSplitViewItem), splitViewItemWithViewController:&*view_controller.objc]) - }; - SplitViewItem { - objc, + objc: unsafe { + ShareId::from_ptr(msg_send![class!(NSSplitViewItem), + splitViewItemWithViewController:&*view_controller.objc + ]) + }, + view_controller } } + /// Creates and returns a new `SplitViewItem`. The returned item is optimized to be a + /// "sidebar"; that is, a typically left-most view that should be treated as such. + /// + /// On macOS Big Sur, this automatically gets the vibrancy backed sidebar view and will extend + /// extend to the top of the window provided the other necessary window flags are set. On macOS + /// versions prior to Big Sur, this returns a standard SplitViewItem. pub fn sidebar(view: T) -> Self { + if !os::is_minimum_version(11) { + return Self::item(view); + } + let view_controller = ViewController::new(view); - let objc = unsafe { - ShareId::from_ptr(msg_send![class!(NSSplitViewItem), sidebarWithViewController:&*view_controller.objc]) - }; - SplitViewItem { - objc, + objc: unsafe { + ShareId::from_ptr(msg_send![class!(NSSplitViewItem), + sidebarWithViewController:&*view_controller.objc + ]) + }, + view_controller } } + /// Sets the titlebar separator style for this `SplitView`. + /// + /// You'd use this if, say, you wanted a border under one part of the `SplitViewController` but + /// not the other. This API was introduced in macOS 11.0 (Big Sur) and is a noop on anything + /// prior. + #[cfg(feature = "macos")] pub fn set_titlebar_separator_style(&self, style: crate::foundation::NSInteger) { - unsafe { - let _: () = msg_send![&*self.objc, setTitlebarSeparatorStyle:style]; + if os::is_minimum_version(11) { + unsafe { + let _: () = msg_send![&*self.objc, setTitlebarSeparatorStyle:style]; + } } } } +/// A SplitViewController manages two or more view controllers in a split-pane view. +/// +/// You typically use this controller as a content view controller for a `Window`. With it, you can +/// build interfaces like those found in Mail.app or Xcode. Dividers can be configured to save +/// their positions so that users can adjust them as they please. +/// +/// Note that the third pane is optional; you can opt to leave it `None`, in which case there's no +/// allocation there, or you can set a placeholder and use it as a details pane. +/// +/// A note on property names: the Cocoa(Touch) controllers tend to view these as: +/// +/// `|sidebar|details|content|` +/// +/// This pattern fits things such as a the aforementioned apps (e.g, Mail). Cacao takes the +/// position that most apps really end up doing the following, though: +/// +/// `|sidebar|content|details|` +/// +/// where details may or may not be visible (e.g, chat applications often work this way). #[derive(Debug)] -pub struct SplitViewController { +pub struct SplitViewController { + /// A reference to the underlying Objective-C split view controller. pub objc: ShareId, + + /// A reference to the sidebar `SplitViewItem`. pub sidebar: SplitViewItem, + + /// A reference to the content `SplitViewItem`. pub content: SplitViewItem, + + /// An optional reference to the details `SplitViewItem`, if set. + pub details: Option> } -impl SplitViewController +impl SplitViewController where Sidebar: ViewDelegate + 'static, - Content: ViewDelegate + 'static + Content: ViewDelegate + 'static, + Details: ViewDelegate + 'static { - pub fn new(sidebar: Sidebar, content: Content) -> Self { + /// Creates and returns a new `SplitViewController`. + pub fn new(sidebar: Sidebar, content: Content, details: Option
) -> Self { let sidebar = SplitViewItem::sidebar(sidebar); let content = SplitViewItem::item(content); + + let details = match details { + Some(vc) => Some(SplitViewItem::item(vc)), + None => None + }; let objc = unsafe { let vc: id = msg_send![class!(NSSplitViewController), new]; let _: () = msg_send![vc, addSplitViewItem:&*sidebar.objc]; let _: () = msg_send![vc, addSplitViewItem:&*content.objc]; + + if let Some(details) = &details { + let _: () = msg_send![vc, addSplitViewItem:&*details.objc]; + } + ShareId::from_ptr(vc) }; - SplitViewController { objc, sidebar, content } + SplitViewController { objc, sidebar, content, details } } +} +impl SplitViewController { /// Toggles the sidebar, if it exists, with an animation. If there's no sidebar in this split view /// (which is highly unlikely, unless you went out of your way to duck this) then it will do /// nothing. @@ -86,6 +157,10 @@ where } } + /// Sets the autosave name for the underlying `SplitView`. + /// + /// Setting this name causes the system to persist separator locations to a defaults database, + /// and the position(s) will be restored upon the user reopening the application. pub fn set_autosave_name(&self, name: &str) { let name = NSString::new(name); @@ -94,29 +169,9 @@ where let _: () = msg_send![split_view, setAutosaveName:&*name]; } } - - /*/// This method can be used to acquire an item for Toolbar instances that tracks a specified - /// divider (`divider_index`) of this split view. This method is only supported on macOS 11.0+; - /// it will return `None` on 10.15 and below. - /// - /// You should call this and pass + store the item in your Toolbar, and vend it to the system - /// with your `ToolbarDelegate`. - pub fn tracking_separator_toolbar_item(&self, divider_index: usize) -> Option { - if crate::utils::os::is_minimum_version(11) { - unsafe { - let split_view: id = msg_send![&*self.objc, splitView]; - let item: id = msg_send![class!(NSTrackingSeparatorToolbarItem), trackingSeparatorToolbarItemWithIdentifier: - splitView:split_view - dividerIndex:divider_index as NSInteger - ]; - } - } - - None - }*/ } -impl Controller for SplitViewController { +impl Controller for SplitViewController { fn get_backing_node(&self) -> ShareId { self.objc.clone() } diff --git a/src/webview/actions.rs b/src/webview/actions.rs index 59ce7e7..b62b2b5 100644 --- a/src/webview/actions.rs +++ b/src/webview/actions.rs @@ -56,6 +56,8 @@ impl From for OpenPanelParameters { match msg_send![params, allowsDirectories] { YES => true, NO => false, + + #[cfg(not(target_arch = "aarch64"))] _ => { panic!("Invalid value from WKOpenPanelParameters:allowsDirectories"); } } }, @@ -64,6 +66,8 @@ impl From for OpenPanelParameters { match msg_send![params, allowsMultipleSelection] { YES => true, NO => false, + + #[cfg(not(target_arch = "aarch64"))] _ => { panic!("Invalid value from WKOpenPanelParameters:allowsMultipleSelection"); } } } diff --git a/src/webview/class.rs b/src/webview/class.rs index ef5e1c2..2a3be29 100644 --- a/src/webview/class.rs +++ b/src/webview/class.rs @@ -47,8 +47,8 @@ extern fn on_message(this: &Object, _: Sel, _: id, script_me let delegate = load::(this, WEBVIEW_DELEGATE_PTR); unsafe { - let name = NSString::wrap(msg_send![script_message, name]); - let body = NSString::wrap(msg_send![script_message, body]); + let name = NSString::from_retained(msg_send![script_message, name]); + let body = NSString::from_retained(msg_send![script_message, body]); delegate.on_message(name.to_str(), body.to_str()); } } @@ -88,10 +88,10 @@ extern fn run_open_panel(this: &Object, _: Sel, _: id, param Some(u) => { let nsurls: NSArray = u.iter().map(|s| { let s = NSString::new(s); - msg_send![class!(NSURL), URLWithString:s.into_inner()] + msg_send![class!(NSURL), URLWithString:&*s] }).collect::>().into(); - (*handler).call((nsurls.into_inner(),)); + (*handler).call((nsurls.into(),)); }, None => { (*handler).call((nil,)); } @@ -102,12 +102,12 @@ extern fn run_open_panel(this: &Object, _: Sel, _: id, param /// Called when a download has been initiated in the WebView, and when the navigation policy /// response is upgraded to BecomeDownload. Only called when explicitly linked since it's a private /// API. -#[cfg(feature = "webview-downloading")] +#[cfg(feature = "webview-downloading-macos")] extern fn handle_download(this: &Object, _: Sel, download: id, suggested_filename: id, handler: usize) { let delegate = load::(this, WEBVIEW_DELEGATE_PTR); let handler = handler as *const Block<(objc::runtime::BOOL, id), c_void>; - let filename = NSString::wrap(suggested_filename); + let filename = NSString::from_retained(suggested_filename); delegate.run_save_panel(filename.to_str(), move |can_overwrite, path| unsafe { if path.is_none() { @@ -119,7 +119,7 @@ extern fn handle_download(this: &Object, _: Sel, download: i (*handler).call((match can_overwrite { true => YES, false => NO - }, path.into_inner())); + }, path.into())); }); } @@ -166,7 +166,7 @@ pub fn register_webview_delegate_class() -> *const Class { // WKDownloadDelegate is a private class on macOS that handles downloading (saving) files. // It's absurd that this is still private in 2020. This probably couldn't get into the app // store, so... screw it, feature-gate it. - #[cfg(feature = "webview-downloading")] + #[cfg(feature = "webview-downloading-macos")] decl.add_method(sel!(_download:decideDestinationWithSuggestedFilename:completionHandler:), handle_download:: as extern fn(&Object, _, id, id, usize)); VIEW_CLASS = decl.register(); diff --git a/src/webview/config.rs b/src/webview/config.rs index 37cc597..19dfdc6 100644 --- a/src/webview/config.rs +++ b/src/webview/config.rs @@ -10,6 +10,7 @@ use crate::webview::enums::InjectAt; /// A wrapper for `WKWebViewConfiguration`. Holds (retains) pointers for the Objective-C runtime /// where everything lives. +#[derive(Debug)] pub struct WebViewConfig { pub objc: Id, pub handlers: Vec diff --git a/src/webview/enums.rs b/src/webview/enums.rs index 8cb3d33..fbbc702 100644 --- a/src/webview/enums.rs +++ b/src/webview/enums.rs @@ -85,7 +85,7 @@ pub enum NavigationResponsePolicy { /// This is a private API, and likely won't make it into the App Store. Will only be available /// if you opt in via the `webview-downloading` feature. - #[cfg(feature = "webview-downloading")] + #[cfg(feature = "webview-downloading-macos")] BecomeDownload } @@ -95,7 +95,7 @@ impl From for NSInteger { NavigationResponsePolicy::Cancel => 0, NavigationResponsePolicy::Allow => 1, - #[cfg(feature = "webview-downloading")] + #[cfg(feature = "webview-downloading-macos")] NavigationResponsePolicy::BecomeDownload => 2 } } diff --git a/src/webview/mod.rs b/src/webview/mod.rs index 7fc9042..632d0cc 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -52,9 +52,9 @@ fn allocate_webview( if let Some(delegate) = &objc_delegate { // Technically private! - #[cfg(feature = "webview-downloading")] + #[cfg(feature = "webview-downloading-macos")] let process_pool: id = msg_send![configuration, processPool]; - #[cfg(feature = "webview-downloading")] + #[cfg(feature = "webview-downloading-macos")] let _: () = msg_send![process_pool, _setDownloadDelegate:*delegate]; let content_controller: id = msg_send![configuration, userContentController]; @@ -203,7 +203,7 @@ impl WebView { let url = NSString::new(url); unsafe { - let u: id = msg_send![class!(NSURL), URLWithString:url.into_inner()]; + let u: id = msg_send![class!(NSURL), URLWithString:&*url]; let request: id = msg_send![class!(NSURLRequest), requestWithURL:u]; let _: () = msg_send![&*self.objc, loadRequest:request]; }