Ongoing documentation and cleanup work.

This commit is contained in:
Ryan McGrath 2021-03-15 17:09:50 -07:00
parent bc54b49475
commit 696907aa73
No known key found for this signature in database
GPG key ID: DA6CBD9233593DEA
30 changed files with 702 additions and 222 deletions

View file

@ -24,8 +24,12 @@ use crate::macos::FocusRingType;
/// where our `NSButton` lives. /// where our `NSButton` lives.
#[derive(Debug)] #[derive(Debug)]
pub struct Button { pub struct Button {
/// A handle for the underlying Objective-C object.
pub objc: ShareId<Object>, pub objc: ShareId<Object>,
/// A reference to an image, if set. We keep a copy to avoid any ownership snafus.
pub image: Option<Image>, pub image: Option<Image>,
handler: Option<TargetActionHandler>, handler: Option<TargetActionHandler>,
/// A pointer to the Objective-C runtime top layout constraint. /// A pointer to the Objective-C runtime top layout constraint.
@ -60,7 +64,11 @@ impl Button {
let title = NSString::new(text); let title = NSString::new(text);
let view: id = unsafe { 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, setWantsLayer:YES];
let _: () = msg_send![button, setTranslatesAutoresizingMaskIntoConstraints:NO]; let _: () = msg_send![button, setTranslatesAutoresizingMaskIntoConstraints:NO];
button button
@ -81,6 +89,7 @@ impl Button {
} }
} }
/// Sets an image on the underlying button.
pub fn set_image(&mut self, image: Image) { pub fn set_image(&mut self, image: Image) {
unsafe { unsafe {
let _: () = msg_send![&*self.objc, setImage:&*image.0]; 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) { pub fn set_key_equivalent(&self, key: &str) {
let key = NSString::new(key); let key = NSString::new(key);
@ -241,19 +252,47 @@ fn register_class() -> *const Class {
#[cfg(feature = "macos")] #[cfg(feature = "macos")]
#[derive(Debug)] #[derive(Debug)]
pub enum BezelStyle { pub enum BezelStyle {
/// A standard circular button.
Circular, Circular,
/// A standard disclosure style button.
Disclosure, Disclosure,
/// The standard looking "Help" (?) button.
HelpButton, HelpButton,
/// An inline style, varies across OS's.
Inline, Inline,
/// A recessed style, varies slightly across OS's.
Recessed, Recessed,
/// A regular square style, with no special styling.
RegularSquare, RegularSquare,
/// A standard rounded rectangle.
RoundRect, RoundRect,
/// A standard rounded button.
Rounded, Rounded,
/// A standard rounded disclosure button.
RoundedDisclosure, RoundedDisclosure,
/// A shadowless square styl.e
ShadowlessSquare, ShadowlessSquare,
/// A small square style.
SmallSquare, SmallSquare,
/// A textured rounded style.
TexturedRounded, TexturedRounded,
/// A textured square style.
TexturedSquare, TexturedSquare,
/// Any style that's not known by this framework (e.g, if Apple
/// introduces something new).
Unknown(NSUInteger) Unknown(NSUInteger)
} }

View file

@ -3,12 +3,22 @@
use crate::foundation::NSUInteger; use crate::foundation::NSUInteger;
/// Flags that indicate a key is in the mix for an event.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum EventModifierFlag { pub enum EventModifierFlag {
/// CapsLock (or shift... oddly named...) is held.
CapsLock, CapsLock,
/// Control is held.
Control, Control,
/// Option is held.
Option, Option,
/// Command (CMD) is held.
Command, Command,
/// Device independent flags mask.
DeviceIndependentFlagsMask 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)] #[derive(Clone, Copy, Debug)]
pub enum EventType { pub enum EventType {
/// A keydown event.
KeyDown KeyDown
} }

View file

@ -2,15 +2,31 @@
use crate::foundation::{NSInteger, NSUInteger}; use crate::foundation::{NSInteger, NSUInteger};
/// Represents a modal response for macOS modal dialogs.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum ModalResponse { pub enum ModalResponse {
/// The user hit the "Ok" button.
Ok, Ok,
/// Continue.
Continue, Continue,
/// Canceled.
Canceled, Canceled,
/// Stopped.
Stopped, Stopped,
/// Aborted.
Aborted, Aborted,
/// The first button in the dialog was clicked.
FirstButtonReturned, FirstButtonReturned,
/// The second button in the dialog was clicked.
SecondButtonReturned, SecondButtonReturned,
/// The third button in the dialog was clicked.
ThirdButtonReturned ThirdButtonReturned
} }
@ -34,12 +50,22 @@ impl From<NSInteger> for ModalResponse {
} }
} }
/// Represents a type of search path used in file manager calls.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum SearchPathDomainMask { pub enum SearchPathDomainMask {
/// User files and folders.
User, User,
/// Local volume files and folders.
Local, Local,
/// Netowrk files and folders.
Network, Network,
/// Search all domains. Not typically used these days.
Domain, Domain,
/// Search all domains. Not typically used these days.
AllDomains AllDomains
} }
@ -55,34 +81,95 @@ impl From<SearchPathDomainMask> 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)] #[derive(Copy, Clone, Debug)]
pub enum SearchPathDirectory { pub enum SearchPathDirectory {
/// The applications folder.
Applications, Applications,
/// Unsupported applications and demo versions. Not generally used these days.
DemoApplications, DemoApplications,
/// Developer applications (_/Developer/Applications_). Not generally used these days.
DeveloperApplications, DeveloperApplications,
/// System and network admin apps.
AdminApplications, AdminApplications,
/// User-visible docs, support, and config files.
Library, Library,
/// Dev resources. (_/Developer_)
Developer, Developer,
/// User home directories. (_/Users_)
User, User,
/// Documentation.
Documentation, Documentation,
/// Documents directory.
Documents, Documents,
/// Core Services (_/System/Library/CoreServices_)
CoreServices, CoreServices,
/// User's autosaved documents (_/Library/Autosave Information_)
AutosavedInformation, AutosavedInformation,
/// The current user's Desktop directory.
Desktop, Desktop,
/// Discardable cache files. (_/Library/Caches_)
Caches, Caches,
/// App support files (_/Library/Application Support_)
ApplicationSupport, ApplicationSupport,
/// The curent user's Downloads directory.
Downloads, Downloads,
/// Input methods (_/Library/Input Methods_)
InputMethods, InputMethods,
/// The current user's Movies directory.
Movies, Movies,
/// The current user's Music directory.
Music, Music,
/// The current user's pictures directory.
Pictures, Pictures,
/// System PPD files (_/Library/Printers/PPDs_)
PrinterDescription, PrinterDescription,
/// The current user's public sharing directory.
SharedPublic, SharedPublic,
/// The Preferences Pane directory, where system preferences files live.
/// (_/Library/PreferencePanes_)
PreferencePanes, PreferencePanes,
/// The user scripts folder for the calling application
/// (_~/Library/Application Scripts/<code-signing-id>_).
ApplicationScripts, ApplicationScripts,
/// Constant used in creating a temp directory.
ItemReplacement, ItemReplacement,
/// All directories where apps can be stored.
AllApplications, AllApplications,
/// All directories where resources can be stored.
AllLibraries, AllLibraries,
/// The Trash directory.
Trash Trash
} }
@ -112,7 +199,6 @@ impl From<SearchPathDirectory> for NSUInteger {
SearchPathDirectory::SharedPublic => 21, SearchPathDirectory::SharedPublic => 21,
SearchPathDirectory::PreferencePanes => 22, SearchPathDirectory::PreferencePanes => 22,
SearchPathDirectory::ApplicationScripts => 23, SearchPathDirectory::ApplicationScripts => 23,
SearchPathDirectory::ItemReplacement => 99, SearchPathDirectory::ItemReplacement => 99,
SearchPathDirectory::AllApplications => 100, SearchPathDirectory::AllApplications => 100,
SearchPathDirectory::AllLibraries => 101, SearchPathDirectory::AllLibraries => 101,

View file

@ -2,7 +2,7 @@
//! tricky, and this transparently handles it for you). //! tricky, and this transparently handles it for you).
use std::error::Error; use std::error::Error;
use std::sync::RwLock; use std::sync::{Arc, RwLock};
use objc_id::Id; use objc_id::Id;
use objc::runtime::{BOOL, Object}; use objc::runtime::{BOOL, Object};
@ -10,47 +10,51 @@ use objc::{class, msg_send, sel, sel_impl};
use url::Url; use url::Url;
use crate::foundation::{id, nil, NO, NSString, NSUInteger}; 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}; use crate::filesystem::enums::{SearchPathDirectory, SearchPathDomainMask};
#[derive(Debug)] /// A FileManager can be used for file operations (moving files, etc).
pub struct FileManager { ///
pub manager: RwLock<Id<Object>> /// 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<RwLock<Id<Object>>>);
impl Default for FileManager { impl Default for FileManager {
/// Returns a default file manager, which maps to the default system file manager. For common /// 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. /// and simple tasks, with no callbacks, you might want this.
fn default() -> Self { fn default() -> Self {
FileManager { FileManager(Arc::new(RwLock::new(unsafe {
manager: RwLock::new(unsafe { let manager: id = msg_send![class!(NSFileManager), defaultManager];
let manager: id = msg_send![class!(NSFileManager), defaultManager]; Id::from_ptr(manager)
Id::from_ptr(manager) })))
})
}
} }
} }
impl FileManager { impl FileManager {
/// Returns a new FileManager that opts in to delegate methods. /// Returns a new FileManager that opts in to delegate methods.
pub fn new() -> Self { pub fn new() -> Self {
FileManager { FileManager(Arc::new(RwLock::new(unsafe {
manager: RwLock::new(unsafe { let manager: id = msg_send![class!(NSFileManager), new];
let manager: id = msg_send![class!(NSFileManager), new]; Id::from_ptr(manager)
Id::from_ptr(manager) })))
})
}
} }
/// Given a directory/domain combination, will attempt to get the directory that matches. /// 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 /// 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. /// side, we attempt to catch it and bubble it up.
pub fn get_directory(&self, directory: SearchPathDirectory, in_domain: SearchPathDomainMask) -> Result<Url, Box<dyn Error>> { pub fn get_directory(
&self,
directory: SearchPathDirectory,
in_domain: SearchPathDomainMask
) -> Result<Url, Box<dyn Error>> {
let dir: NSUInteger = directory.into(); let dir: NSUInteger = directory.into();
let mask: NSUInteger = in_domain.into(); let mask: NSUInteger = in_domain.into();
let directory = unsafe { let directory = unsafe {
let manager = self.manager.read().unwrap(); let manager = self.0.read().unwrap();
let dir: id = msg_send![&**manager, URLForDirectory:dir let dir: id = msg_send![&**manager, URLForDirectory:dir
inDomain:mask inDomain:mask
appropriateForURL:nil appropriateForURL:nil
@ -76,7 +80,7 @@ impl FileManager {
// This should potentially be write(), but the backing class handles this logic // This should potentially be write(), but the backing class handles this logic
// already, so... going to leave it as read. // 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 error: id = nil;
let result: BOOL = msg_send![&**manager, moveItemAtURL:from_url toURL:to_url error:&error]; let result: BOOL = msg_send![&**manager, moveItemAtURL:from_url toURL:to_url error:&error];

View file

@ -47,12 +47,23 @@ impl FileSavePanel {
} }
} }
/// @TODO: Do we even need this?
pub fn set_delegate(&mut self) {} 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<S: AsRef<str>>(&mut self, suggested_filename: S) {
unsafe { unsafe {
let filename = NSString::new(suggested_filename); let filename = NSString::new(suggested_filename.as_ref());
let _: () = msg_send![&*self.panel, setNameFieldStringValue:filename]; let _: () = msg_send![&*self.panel, setNameFieldStringValue:&*filename];
}
}
/// Set the message text displayed in the panel.
pub fn set_message<S: AsRef<str>>(&mut self, message: S) {
unsafe {
let message = NSString::new(message.as_ref());
let _: () = msg_send![&*self.panel, setMessage:&*message];
} }
} }

View file

@ -82,6 +82,14 @@ impl FileSelectPanel {
self.can_choose_files = can_choose; self.can_choose_files = can_choose;
} }
/// Set the message text displayed in the panel.
pub fn set_message<S: AsRef<str>>(&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. /// Sets whether the user can choose directories.
pub fn set_can_choose_directories(&mut self, can_choose: bool) { pub fn set_can_choose_directories(&mut self, can_choose: bool) {
unsafe { unsafe {

View file

@ -17,6 +17,7 @@ const UTF8_ENCODING: usize = 4;
/// side is fairly battle tested. /// side is fairly battle tested.
#[derive(Debug)] #[derive(Debug)]
pub struct NSString<'a> { pub struct NSString<'a> {
/// A reference to the backing `NSString`.
pub objc: Id<Object>, pub objc: Id<Object>,
phantom: PhantomData<&'a ()> phantom: PhantomData<&'a ()>
} }

View file

@ -15,11 +15,19 @@ use crate::foundation::{id, YES, NO, NSString};
use crate::utils::os; use crate::utils::os;
use super::icons::*; use super::icons::*;
#[derive(Debug)] /// Specifies resizing behavior for image drawing.
#[derive(Copy, Clone, Debug)]
pub enum ResizeBehavior { pub enum ResizeBehavior {
/// Fit to the aspect ratio.
AspectFit, AspectFit,
/// Fill the aspect ratio.
AspectFill, AspectFill,
/// Stretch as necessary.
Stretch, Stretch,
/// Center and then let whatever else flow around it.
Center Center
} }
@ -42,6 +50,8 @@ fn min_cgfloat(x: CGFloat, y: CGFloat) -> CGFloat {
} }
impl ResizeBehavior { 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 { pub fn apply(&self, source: CGRect, target: CGRect) -> CGRect {
// if equal, just return source // if equal, just return source
if 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 { pub struct DrawConfig {
/// The size of the source.
pub source: (CGFloat, CGFloat), 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), pub target: (CGFloat, CGFloat),
/// The type of resizing to use during drawing and scaling.
pub resize: ResizeBehavior 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)] #[derive(Clone, Debug)]
pub struct Image(pub ShareId<Object>); pub struct Image(pub ShareId<Object>);
@ -195,7 +214,11 @@ impl Image {
let block = block.copy(); let block = block.copy();
Image(unsafe { 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) ShareId::from_ptr(img)
}) })
} }

View file

@ -223,6 +223,7 @@ impl<T> TextField<T> {
} }
} }
/// The the text alignment style for this control.
pub fn set_text_alignment(&self, alignment: TextAlign) { pub fn set_text_alignment(&self, alignment: TextAlign) {
unsafe { unsafe {
let alignment: NSInteger = alignment.into(); let alignment: NSInteger = alignment.into();
@ -232,7 +233,7 @@ impl<T> TextField<T> {
/// Sets the font for this input. /// Sets the font for this input.
pub fn set_font<F: AsRef<Font>>(&self, font: F) { pub fn set_font<F: AsRef<Font>>(&self, font: F) {
let font = font.as_ref(); let font = font.as_ref().clone();
unsafe { unsafe {
let _: () = msg_send![&*self.objc, setFont:&*font]; let _: () = msg_send![&*self.objc, setFont:&*font];

View file

@ -208,7 +208,12 @@ impl From<NSUInteger> for LayoutFormat {
/// Specifies layout priority. /// Specifies layout priority.
#[derive(Debug)] #[derive(Debug)]
pub enum LayoutPriority { pub enum LayoutPriority {
/// Highest priority.
Required, Required,
/// High priority. Will bend if absolutely necessary.
High, High,
/// Low priority.
Low Low
} }

View file

@ -42,9 +42,15 @@ impl Into<NSUInteger> for RowAnimation {
} }
} }
/// Specifies a row edge.
///
/// Generally used to indicate where row actions (swipe-to-reveal) should appear.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum RowEdge { pub enum RowEdge {
/// The leading edge.
Leading, Leading,
/// The trailing edge.
Trailing Trailing
} }

View file

@ -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::foundation::{load_or_register_class, id, nil, YES, NO, NSArray, NSInteger, NSUInteger};
use crate::dragdrop::DragInfo; use crate::dragdrop::DragInfo;
use crate::listview::{ use crate::listview::{
LISTVIEW_DELEGATE_PTR, LISTVIEW_CELL_VENDOR_PTR, LISTVIEW_DELEGATE_PTR,
ListViewDelegate, RowEdge ListViewDelegate, RowEdge
}; };
use crate::utils::load; use crate::utils::load;
@ -197,7 +197,6 @@ pub(crate) fn register_listview_class() -> *const Class {
pub(crate) fn register_listview_class_with_delegate<T: ListViewDelegate>(instance: &T) -> *const Class { pub(crate) fn register_listview_class_with_delegate<T: ListViewDelegate>(instance: &T) -> *const Class {
load_or_register_class("NSTableView", instance.subclass_name(), |decl| unsafe { load_or_register_class("NSTableView", instance.subclass_name(), |decl| unsafe {
decl.add_ivar::<usize>(LISTVIEW_DELEGATE_PTR); 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); decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);

View file

@ -53,7 +53,8 @@ use crate::color::Color;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::pasteboard::PasteboardType; use crate::pasteboard::PasteboardType;
use crate::scrollview::ScrollView; use crate::scrollview::ScrollView;
use crate::utils::CGSize; use crate::utils::{os, CellFactory, CGSize};
use crate::view::ViewDelegate;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use crate::macos::menu::MenuItem; use crate::macos::menu::MenuItem;
@ -83,7 +84,6 @@ mod actions;
pub use actions::{RowAction, RowActionStyle}; pub use actions::{RowAction, RowActionStyle};
pub(crate) static LISTVIEW_DELEGATE_PTR: &str = "rstListViewDelegatePtr"; pub(crate) static LISTVIEW_DELEGATE_PTR: &str = "rstListViewDelegatePtr";
pub(crate) static LISTVIEW_CELL_VENDOR_PTR: &str = "rstListViewCellVendorPtr";
use std::any::Any; use std::any::Any;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
@ -91,57 +91,6 @@ use std::sync::{Arc, RwLock};
use std::rc::Rc; use std::rc::Rc;
use std::cell::RefCell; use std::cell::RefCell;
use crate::view::ViewDelegate;
pub(crate) type CellFactoryMap = HashMap<&'static str, Box<dyn Fn() -> Box<dyn 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<dyn 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. /// A helper method for instantiating view classes and applying default settings to them.
fn common_init(class: *const Class) -> id { fn common_init(class: *const Class) -> id {
unsafe { unsafe {
@ -232,7 +181,7 @@ pub struct ListView<T = ()> {
/// allocation and reuse, which is necessary for an "infinite" listview. /// allocation and reuse, which is necessary for an "infinite" listview.
cell_factory: CellFactory, cell_factory: CellFactory,
pub menu: PropertyNullable<Vec<MenuItem>>, menu: PropertyNullable<Vec<MenuItem>>,
/// A pointer to the Objective-C runtime view controller. /// A pointer to the Objective-C runtime view controller.
pub objc: ShareId<Object>, pub objc: ShareId<Object>,
@ -277,7 +226,7 @@ impl Default for ListView {
} }
impl ListView { impl ListView {
/// Returns a default `View`, suitable for /// @TODO: The hell is this for?
pub fn new() -> Self { pub fn new() -> Self {
let class = register_listview_class(); let class = register_listview_class();
let view = common_init(class); let view = common_init(class);
@ -333,9 +282,7 @@ impl<T> ListView<T> where T: ListViewDelegate + 'static {
//let view: id = msg_send![register_view_class_with_delegate::<T>(), new]; //let view: id = msg_send![register_view_class_with_delegate::<T>(), new];
//let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; //let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
let delegate_ptr: *const T = &*delegate; 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_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, setDelegate:view];
let _: () = msg_send![view, setDataSource:view]; let _: () = msg_send![view, setDataSource:view];
}; };
@ -446,13 +393,23 @@ impl<T> ListView<T> {
} }
} }
/// 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) { pub fn set_style(&self, style: crate::foundation::NSInteger) {
unsafe { if os::is_minimum_version(11) {
let _: () = msg_send![&*self.objc, setStyle:style]; 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) { pub fn set_allows_empty_selection(&self, allows: bool) {
unsafe { unsafe {
let _: () = msg_send![&*self.objc, setAllowsEmptySelection:match allows { let _: () = msg_send![&*self.objc, setAllowsEmptySelection:match allows {
@ -462,12 +419,14 @@ impl<T> ListView<T> {
} }
} }
/// Set the selection highlight style.
pub fn set_selection_highlight_style(&self, style: crate::foundation::NSInteger) { pub fn set_selection_highlight_style(&self, style: crate::foundation::NSInteger) {
unsafe { unsafe {
let _: () = msg_send![&*self.objc, setSelectionHighlightStyle:style]; 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) { pub fn select_row_indexes(&self, indexes: &[usize], extends_existing: bool) {
unsafe { unsafe {
let index_set: id = msg_send![class!(NSMutableIndexSet), new]; let index_set: id = msg_send![class!(NSMutableIndexSet), new];
@ -483,6 +442,16 @@ impl<T> ListView<T> {
} }
} }
/// 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<F: Fn(ListView)>(&self, update: F) { pub fn perform_batch_updates<F: Fn(ListView)>(&self, update: F) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
unsafe { unsafe {
@ -495,6 +464,11 @@ impl<T> ListView<T> {
} }
} }
/// 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) { pub fn insert_rows(&self, indexes: &[usize], animation: RowAnimation) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
unsafe { unsafe {
@ -514,6 +488,7 @@ impl<T> ListView<T> {
} }
} }
/// Reload the rows at the specified indexes.
pub fn reload_rows(&self, indexes: &[usize]) { pub fn reload_rows(&self, indexes: &[usize]) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
unsafe { unsafe {
@ -532,6 +507,11 @@ impl<T> ListView<T> {
} }
} }
/// 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) { pub fn remove_rows(&self, indexes: &[usize], animations: RowAnimation) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
unsafe { unsafe {
@ -612,16 +592,43 @@ impl<T> ListView<T> {
} }
} }
/// 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) { pub fn reload(&self) {
unsafe { unsafe {
let _: () = msg_send![&*self.objc, reloadData]; let _: () = msg_send![&*self.objc, reloadData];
} }
} }
/// Returns the selected row.
pub fn get_selected_row_index(&self) -> NSInteger { pub fn get_selected_row_index(&self) -> NSInteger {
unsafe { msg_send![&*self.objc, selectedRow] } 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<MenuItem> {
/// 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 { pub fn get_clicked_row_index(&self) -> NSInteger {
unsafe { msg_send![&*self.objc, clickedRow] } unsafe { msg_send![&*self.objc, clickedRow] }
} }

View file

@ -83,10 +83,19 @@ fn shared_application<F: Fn(id)>(handler: F) {
/// implement the `Dispatcher` trait to receive messages that you might dispatch from deeper in the /// implement the `Dispatcher` trait to receive messages that you might dispatch from deeper in the
/// application. /// application.
pub struct App<T = (), M = ()> { pub struct App<T = (), M = ()> {
/// The underlying Objective-C Object.
pub objc: Id<Object>, pub objc: Id<Object>,
/// The underlying Objective-C Object, which in this case is a delegate that forwards to the
/// app delegate.
pub objc_delegate: Id<Object>, pub objc_delegate: Id<Object>,
/// The stored `AppDelegate`.
pub delegate: Box<T>, pub delegate: Box<T>,
/// The main-thread AutoReleasePool. Drains on app exit.
pub pool: AutoReleasePool, pub pool: AutoReleasePool,
_message: std::marker::PhantomData<M> _message: std::marker::PhantomData<M>
} }

View file

@ -1,34 +1,91 @@
use objc::{class, msg_send, sel, sel_impl}; 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)] #[derive(Debug)]
pub enum CursorType { pub enum CursorType {
/// A standard arrow.
Arrow, Arrow,
/// A crosshair.
Crosshair, Crosshair,
/// A closed hand, typically for mousedown and drag.
ClosedHand, ClosedHand,
/// An open hand, typically for indicating draggable.
OpenHand, OpenHand,
/// A pointing hand, like clicking a link.
PointingHand, PointingHand,
/// Indicator that something can be resized to the left.
ResizeLeft, ResizeLeft,
/// Indicator that something can be resized to the right.
ResizeRight, ResizeRight,
/// Indicator that something can be resized on the horizontal axis.
ResizeLeftRight, ResizeLeftRight,
/// Indicates that something can be resized up.
ResizeUp, ResizeUp,
/// Indicates that something can be resized down.
ResizeDown, ResizeDown,
/// Indicator that something can be resized on the vertical axis.
ResizeUpDown, ResizeUpDown,
/// Otherwise known as the "poof" or "cloud" cursor. Indicates something will vanish, like
/// dragging into the Trash.
DisappearingItem, DisappearingItem,
/// Indicate an insertion point, like for text.
IBeam, IBeam,
/// The vertical version of `CursorType::IBeam`.
IBeamVertical, IBeamVertical,
/// Indicates an operation is illegal.
OperationNotAllowed, OperationNotAllowed,
/// The drag link cursor.
DragLink, DragLink,
/// Used for drag-and-drop usually, will displayu the standard "+" icon next to the cursor.
DragCopy, DragCopy,
/// Indicates a context menu will open.
ContextMenu 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)] #[derive(Debug)]
pub struct Cursor; pub struct Cursor;
@ -82,4 +139,19 @@ impl Cursor {
let _: () = msg_send![class!(NSCursor), unhide]; 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
}];
}
}
} }

View file

@ -1,28 +1,54 @@
//! A lightweight wrapper over some networking components, like `NSURLRequest` and co. //! 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::{msg_send, sel, sel_impl};
use objc::runtime::Object; use objc::runtime::Object;
use objc_id::Id; use objc_id::ShareId;
use crate::foundation::{id, NSString}; use crate::foundation::{id, NSString};
/// A wrapper around `NSURLRequest`.
#[derive(Debug)] #[derive(Debug)]
pub struct URLRequest { pub struct URLRequest(ShareId<Object>);
pub inner: Id<Object>
}
impl URLRequest { impl URLRequest {
pub fn with(inner: id) -> Self { /// Wraps and retains an `NSURLRequest`.
URLRequest { pub fn with(request: id) -> Self {
inner: unsafe { Id::from_ptr(inner) } 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 { NSString::from_retained(unsafe {
let url: id = msg_send![&*self.inner, URL]; let url: id = msg_send![&*self.0, URL];
msg_send![url, absoluteString] msg_send![url, absoluteString]
}).to_string() }).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);
}
}

View file

@ -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::<YourAppDelegate, YourMessageType>::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 { pub trait Dispatcher {
/// The type of Message you're sending. This should be lightweight and thread safe.
type Message: Send + Sync; 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) {} 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) {} fn on_background_message(&self, _message: Self::Message) {}
} }

View file

@ -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 }
}

View file

@ -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_id::ShareId;
use objc::runtime::{Class, Object}; 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::foundation::{id, nil, YES, NO, NSUInteger};
use crate::color::Color; use crate::color::Color;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; 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")] #[cfg(target_os = "ios")]
mod ios; mod ios;
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
use ios::{register_progress_indicator_class}; use ios::register_progress_indicator_class;
mod enums; mod enums;
pub use enums::ProgressIndicatorStyle; pub use enums::ProgressIndicatorStyle;
/// A control used for reporting progress to a user visually.
#[derive(Debug)] #[derive(Debug)]
pub struct ProgressIndicator { pub struct ProgressIndicator {
/// A pointer to the Objective-C runtime view controller. /// A pointer to the Objective-C Object.
pub objc: ShareId<Object>, pub objc: ShareId<Object>,
/// A pointer to the Objective-C runtime top layout constraint. /// A pointer to the Objective-C top layout constraint.
pub top: LayoutAnchorY, 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, 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, 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, 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, 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, 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, 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 pub center_y: LayoutAnchorY
} }
@ -58,13 +68,15 @@ impl Default for ProgressIndicator {
} }
impl 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 { pub fn new() -> Self {
let view = unsafe { 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]; let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
#[cfg(target_os = "macos")] #[cfg(feature = "macos")]
let _: () = msg_send![view, setWantsLayer:YES]; let _: () = msg_send![view, setWantsLayer:YES];
view view
@ -85,17 +97,6 @@ impl ProgressIndicator {
} }
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. /// Starts the animation for an indeterminate indicator.
pub fn start_animation(&self) { pub fn start_animation(&self) {
unsafe { 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) { pub fn stop_animation(&self) {
unsafe { unsafe {
let _: () = msg_send![&*self.objc, stopAnimation:nil]; 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 { 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) { pub fn set_style(&self, style: ProgressIndicatorStyle) {
unsafe { unsafe {
let style = style as NSUInteger; 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) { pub fn set_indeterminate(&self, is_indeterminate: bool) {
unsafe { unsafe {
let _: () = msg_send![&*self.objc, setIndeterminate:match is_indeterminate { 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 { impl Layout for ProgressIndicator {
@ -147,20 +177,19 @@ impl Layout for ProgressIndicator {
} }
impl Drop 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 /// 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). /// 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. /// There are, thankfully, no delegates we need to break here.
fn drop(&mut self) { fn drop(&mut self) {
/*if self.delegate.is_some() { unsafe {
unsafe { let superview: id = msg_send![&*self.objc, superview];
let superview: id = msg_send![&*self.objc, superview]; if superview != nil {
if superview != nil { let _: () = msg_send![&*self.objc, removeFromSuperview];
let _: () = msg_send![&*self.objc, removeFromSuperview];
}
} }
}*/ }
} }
} }

View file

@ -1,6 +1,9 @@
use crate::dragdrop::{DragInfo, DragOperation}; use crate::dragdrop::{DragInfo, DragOperation};
use crate::scrollview::ScrollView; 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 { pub trait ScrollViewDelegate {
/// Called when the View is ready to work with. You're passed a `View` - this is safe to /// 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 /// store and use repeatedly, but it's not thread safe - any UI calls must be made from the

View file

@ -18,6 +18,7 @@ use crate::utils::load;
/// where our `NSSwitch` lives. /// where our `NSSwitch` lives.
#[derive(Debug)] #[derive(Debug)]
pub struct Switch { pub struct Switch {
/// A pointer to the underlying Objective-C Object.
pub objc: ShareId<Object>, pub objc: ShareId<Object>,
handler: Option<TargetActionHandler>, handler: Option<TargetActionHandler>,

View file

@ -223,7 +223,8 @@ impl<T> Label<T> {
} }
/// Call this to set the text for the label. /// Call this to set the text for the label.
pub fn set_text(&self, text: &str) { pub fn set_text<S: AsRef<str>>(&self, text: S) {
let text = text.as_ref();
let s = NSString::new(text); let s = NSString::new(text);
unsafe { unsafe {
@ -269,12 +270,20 @@ impl<T> Label<T> {
} }
} }
/// 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. /// Set the line break mode for this label.
pub fn set_line_break_mode(&self, mode: LineBreakMode) { pub fn set_line_break_mode(&self, mode: LineBreakMode) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
unsafe { unsafe {
let cell: id = msg_send![&*self.objc, cell]; let cell: id = msg_send![&*self.objc, cell];
let mode = mode as NSUInteger; let mode = mode as NSUInteger;
let _: () = msg_send![cell, setTruncatesLastVisibleLine:YES];
let _: () = msg_send![cell, setLineBreakMode:mode]; let _: () = msg_send![cell, setLineBreakMode:mode];
} }
} }

71
src/utils/cell_factory.rs Normal file
View file

@ -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<dyn Fn() -> Box<dyn Any>>>;
/// 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<RefCell<CellFactoryMap>>);
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<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<dyn Any>
}));
}
/// 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<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 for {}: 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);
}
}
}
impl std::fmt::Debug for CellFactory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CellFactory").finish()
}
}

View file

@ -12,6 +12,9 @@ use objc_id::ShareId;
use crate::foundation::{id, BOOL, YES, NO}; use crate::foundation::{id, BOOL, YES, NO};
mod cell_factory;
pub use cell_factory::CellFactory;
pub mod os; pub mod os;
/// A generic trait that's used throughout multiple different controls in this framework - acts as /// A generic trait that's used throughout multiple different controls in this framework - acts as

View file

@ -3,14 +3,22 @@ use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, nil, NSString}; use crate::foundation::{id, nil, NSString};
use crate::layout::{Layout}; use crate::layout::Layout;
use crate::macos::toolbar::ToolbarItem; use crate::macos::toolbar::ToolbarItem;
use crate::view::{View, ViewController, ViewDelegate}; 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)] #[derive(Debug)]
pub struct SplitViewItem<T> { pub struct SplitViewItem<T> {
/// The underlying Objective-C Object.
pub objc: ShareId<Object>, pub objc: ShareId<Object>,
/// The wrapped ViewController.
pub view_controller: ViewController<T> pub view_controller: ViewController<T>
} }
@ -18,65 +26,128 @@ impl<T> SplitViewItem<T>
where where
T: ViewDelegate + 'static 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 { pub fn item(view: T) -> Self {
let view_controller = ViewController::new(view); let view_controller = ViewController::new(view);
let objc = unsafe {
ShareId::from_ptr(msg_send![class!(NSSplitViewItem), splitViewItemWithViewController:&*view_controller.objc])
};
SplitViewItem { SplitViewItem {
objc, objc: unsafe {
ShareId::from_ptr(msg_send![class!(NSSplitViewItem),
splitViewItemWithViewController:&*view_controller.objc
])
},
view_controller 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 { pub fn sidebar(view: T) -> Self {
if !os::is_minimum_version(11) {
return Self::item(view);
}
let view_controller = ViewController::new(view); let view_controller = ViewController::new(view);
let objc = unsafe {
ShareId::from_ptr(msg_send![class!(NSSplitViewItem), sidebarWithViewController:&*view_controller.objc])
};
SplitViewItem { SplitViewItem {
objc, objc: unsafe {
ShareId::from_ptr(msg_send![class!(NSSplitViewItem),
sidebarWithViewController:&*view_controller.objc
])
},
view_controller 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) { pub fn set_titlebar_separator_style(&self, style: crate::foundation::NSInteger) {
unsafe { if os::is_minimum_version(11) {
let _: () = msg_send![&*self.objc, setTitlebarSeparatorStyle:style]; 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)] #[derive(Debug)]
pub struct SplitViewController<Sidebar, Content> { pub struct SplitViewController<Sidebar, Content, Details> {
/// A reference to the underlying Objective-C split view controller.
pub objc: ShareId<Object>, pub objc: ShareId<Object>,
/// A reference to the sidebar `SplitViewItem`.
pub sidebar: SplitViewItem<Sidebar>, pub sidebar: SplitViewItem<Sidebar>,
/// A reference to the content `SplitViewItem`.
pub content: SplitViewItem<Content>, pub content: SplitViewItem<Content>,
/// An optional reference to the details `SplitViewItem`, if set.
pub details: Option<SplitViewItem<Details>>
} }
impl<Sidebar, Content> SplitViewController<Sidebar, Content> impl<Sidebar, Content, Details> SplitViewController<Sidebar, Content, Details>
where where
Sidebar: ViewDelegate + 'static, 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<Details>) -> Self {
let sidebar = SplitViewItem::sidebar(sidebar); let sidebar = SplitViewItem::sidebar(sidebar);
let content = SplitViewItem::item(content); let content = SplitViewItem::item(content);
let details = match details {
Some(vc) => Some(SplitViewItem::item(vc)),
None => None
};
let objc = unsafe { let objc = unsafe {
let vc: id = msg_send![class!(NSSplitViewController), new]; let vc: id = msg_send![class!(NSSplitViewController), new];
let _: () = msg_send![vc, addSplitViewItem:&*sidebar.objc]; let _: () = msg_send![vc, addSplitViewItem:&*sidebar.objc];
let _: () = msg_send![vc, addSplitViewItem:&*content.objc]; let _: () = msg_send![vc, addSplitViewItem:&*content.objc];
if let Some(details) = &details {
let _: () = msg_send![vc, addSplitViewItem:&*details.objc];
}
ShareId::from_ptr(vc) ShareId::from_ptr(vc)
}; };
SplitViewController { objc, sidebar, content } SplitViewController { objc, sidebar, content, details }
} }
}
impl<Sidebar, Content, Details> SplitViewController<Sidebar, Content, Details> {
/// Toggles the sidebar, if it exists, with an animation. If there's no sidebar in this split view /// 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 /// (which is highly unlikely, unless you went out of your way to duck this) then it will do
/// nothing. /// 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) { pub fn set_autosave_name(&self, name: &str) {
let name = NSString::new(name); let name = NSString::new(name);
@ -94,29 +169,9 @@ where
let _: () = msg_send![split_view, setAutosaveName:&*name]; 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<ToolbarItem> {
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<Sidebar, Content> Controller for SplitViewController<Sidebar, Content> { impl<Sidebar, Content, Details> Controller for SplitViewController<Sidebar, Content, Details> {
fn get_backing_node(&self) -> ShareId<Object> { fn get_backing_node(&self) -> ShareId<Object> {
self.objc.clone() self.objc.clone()
} }

View file

@ -56,6 +56,8 @@ impl From<id> for OpenPanelParameters {
match msg_send![params, allowsDirectories] { match msg_send![params, allowsDirectories] {
YES => true, YES => true,
NO => false, NO => false,
#[cfg(not(target_arch = "aarch64"))]
_ => { panic!("Invalid value from WKOpenPanelParameters:allowsDirectories"); } _ => { panic!("Invalid value from WKOpenPanelParameters:allowsDirectories"); }
} }
}, },
@ -64,6 +66,8 @@ impl From<id> for OpenPanelParameters {
match msg_send![params, allowsMultipleSelection] { match msg_send![params, allowsMultipleSelection] {
YES => true, YES => true,
NO => false, NO => false,
#[cfg(not(target_arch = "aarch64"))]
_ => { panic!("Invalid value from WKOpenPanelParameters:allowsMultipleSelection"); } _ => { panic!("Invalid value from WKOpenPanelParameters:allowsMultipleSelection"); }
} }
} }

View file

@ -47,8 +47,8 @@ extern fn on_message<T: WebViewDelegate>(this: &Object, _: Sel, _: id, script_me
let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR); let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR);
unsafe { unsafe {
let name = NSString::wrap(msg_send![script_message, name]); let name = NSString::from_retained(msg_send![script_message, name]);
let body = NSString::wrap(msg_send![script_message, body]); let body = NSString::from_retained(msg_send![script_message, body]);
delegate.on_message(name.to_str(), body.to_str()); delegate.on_message(name.to_str(), body.to_str());
} }
} }
@ -88,10 +88,10 @@ extern fn run_open_panel<T: WebViewDelegate>(this: &Object, _: Sel, _: id, param
Some(u) => { Some(u) => {
let nsurls: NSArray = u.iter().map(|s| { let nsurls: NSArray = u.iter().map(|s| {
let s = NSString::new(s); let s = NSString::new(s);
msg_send![class!(NSURL), URLWithString:s.into_inner()] msg_send![class!(NSURL), URLWithString:&*s]
}).collect::<Vec<id>>().into(); }).collect::<Vec<id>>().into();
(*handler).call((nsurls.into_inner(),)); (*handler).call((nsurls.into(),));
}, },
None => { (*handler).call((nil,)); } None => { (*handler).call((nil,)); }
@ -102,12 +102,12 @@ extern fn run_open_panel<T: WebViewDelegate>(this: &Object, _: Sel, _: id, param
/// Called when a download has been initiated in the WebView, and when the navigation policy /// 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 /// response is upgraded to BecomeDownload. Only called when explicitly linked since it's a private
/// API. /// API.
#[cfg(feature = "webview-downloading")] #[cfg(feature = "webview-downloading-macos")]
extern fn handle_download<T: WebViewDelegate>(this: &Object, _: Sel, download: id, suggested_filename: id, handler: usize) { extern fn handle_download<T: WebViewDelegate>(this: &Object, _: Sel, download: id, suggested_filename: id, handler: usize) {
let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR); let delegate = load::<T>(this, WEBVIEW_DELEGATE_PTR);
let handler = handler as *const Block<(objc::runtime::BOOL, id), c_void>; 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 { delegate.run_save_panel(filename.to_str(), move |can_overwrite, path| unsafe {
if path.is_none() { if path.is_none() {
@ -119,7 +119,7 @@ extern fn handle_download<T: WebViewDelegate>(this: &Object, _: Sel, download: i
(*handler).call((match can_overwrite { (*handler).call((match can_overwrite {
true => YES, true => YES,
false => NO false => NO
}, path.into_inner())); }, path.into()));
}); });
} }
@ -166,7 +166,7 @@ pub fn register_webview_delegate_class<T: WebViewDelegate>() -> *const Class {
// WKDownloadDelegate is a private class on macOS that handles downloading (saving) files. // 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 // 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. // 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::<T> as extern fn(&Object, _, id, id, usize)); decl.add_method(sel!(_download:decideDestinationWithSuggestedFilename:completionHandler:), handle_download::<T> as extern fn(&Object, _, id, id, usize));
VIEW_CLASS = decl.register(); VIEW_CLASS = decl.register();

View file

@ -10,6 +10,7 @@ use crate::webview::enums::InjectAt;
/// A wrapper for `WKWebViewConfiguration`. Holds (retains) pointers for the Objective-C runtime /// A wrapper for `WKWebViewConfiguration`. Holds (retains) pointers for the Objective-C runtime
/// where everything lives. /// where everything lives.
#[derive(Debug)]
pub struct WebViewConfig { pub struct WebViewConfig {
pub objc: Id<Object>, pub objc: Id<Object>,
pub handlers: Vec<String> pub handlers: Vec<String>

View file

@ -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 /// 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. /// if you opt in via the `webview-downloading` feature.
#[cfg(feature = "webview-downloading")] #[cfg(feature = "webview-downloading-macos")]
BecomeDownload BecomeDownload
} }
@ -95,7 +95,7 @@ impl From<NavigationResponsePolicy> for NSInteger {
NavigationResponsePolicy::Cancel => 0, NavigationResponsePolicy::Cancel => 0,
NavigationResponsePolicy::Allow => 1, NavigationResponsePolicy::Allow => 1,
#[cfg(feature = "webview-downloading")] #[cfg(feature = "webview-downloading-macos")]
NavigationResponsePolicy::BecomeDownload => 2 NavigationResponsePolicy::BecomeDownload => 2
} }
} }

View file

@ -52,9 +52,9 @@ fn allocate_webview(
if let Some(delegate) = &objc_delegate { if let Some(delegate) = &objc_delegate {
// Technically private! // Technically private!
#[cfg(feature = "webview-downloading")] #[cfg(feature = "webview-downloading-macos")]
let process_pool: id = msg_send![configuration, processPool]; 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 _: () = msg_send![process_pool, _setDownloadDelegate:*delegate];
let content_controller: id = msg_send![configuration, userContentController]; let content_controller: id = msg_send![configuration, userContentController];
@ -203,7 +203,7 @@ impl<T> WebView<T> {
let url = NSString::new(url); let url = NSString::new(url);
unsafe { 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 request: id = msg_send![class!(NSURLRequest), requestWithURL:u];
let _: () = msg_send![&*self.objc, loadRequest:request]; let _: () = msg_send![&*self.objc, loadRequest:request];
} }