Some more updates for macOS.

- Added support for basic Cursor management.
- Added support for NSWindow cancelOperation: callbacks. It's not...
  perfect, but it works as a discrete hook.
- Added support for NSProgressIndicator.
- Properly forward Error types from QuickLook generation calls, and
  future-proof the ThumbnailQuality enum.
- Add support for configuring Label line break mode.
This commit is contained in:
Ryan McGrath 2021-01-19 00:11:52 -08:00
parent 121a2f938e
commit 62cebab691
No known key found for this signature in database
GPG key ID: DA6CBD9233593DEA
17 changed files with 497 additions and 16 deletions

View file

@ -24,6 +24,10 @@ impl From<NSInteger> for ModalResponse {
-1000 => ModalResponse::Stopped,
-1001 => ModalResponse::Aborted,
-1002 => ModalResponse::Continue,
// @TODO: Definitely don't panic here, wtf was I thinking?
// Probably make this a ModalResponse::Unknown or something so a user can
// gracefully handle.
e => { panic!("Unknown NSModalResponse sent back! {}", e); }
}
}

View file

@ -106,6 +106,7 @@ pub mod listview;
pub mod networking;
pub mod notification_center;
pub mod pasteboard;
pub mod progress;
pub mod scrollview;
pub mod text;

85
src/macos/cursor.rs Normal file
View file

@ -0,0 +1,85 @@
use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::id;
#[derive(Debug)]
pub enum CursorType {
Arrow,
Crosshair,
ClosedHand,
OpenHand,
PointingHand,
ResizeLeft,
ResizeRight,
ResizeLeftRight,
ResizeUp,
ResizeDown,
ResizeUpDown,
DisappearingItem,
IBeam,
IBeamVertical,
OperationNotAllowed,
DragLink,
DragCopy,
ContextMenu
}
#[derive(Debug)]
pub struct Cursor;
impl Cursor {
/// Given a cursor type, will make it the system cursor.
/// The inverse of this call, which you should call when ready, is `pop()`.
pub fn push(cursor_type: CursorType) {
unsafe {
let cursor: id = match cursor_type {
CursorType::Arrow => msg_send![class!(NSCursor), arrowCursor],
CursorType::Crosshair => msg_send![class!(NSCursor), crosshairCursor],
CursorType::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
CursorType::OpenHand => msg_send![class!(NSCursor), openHandCursor],
CursorType::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
CursorType::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
CursorType::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
CursorType::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorType::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor],
CursorType::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
CursorType::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorType::DisappearingItem => msg_send![class!(NSCursor), disappearingItemCursor],
CursorType::IBeam => msg_send![class!(NSCursor), IBeamCursor],
CursorType::IBeamVertical => msg_send![class!(NSCursor), IBeamCursorForVerticalLayout],
CursorType::OperationNotAllowed => msg_send![class!(NSCursor), operationNotAllowedCursor],
CursorType::DragLink => msg_send![class!(NSCursor), dragLinkCursor],
CursorType::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
CursorType::ContextMenu => msg_send![class!(NSCursor), contextualMenuCursor]
};
let _: () = msg_send![cursor, push];
}
}
/// Pops the current cursor off the cursor-stack. The inverse of push.
pub fn pop() {
unsafe {
let _: () = msg_send![class!(NSCursor), pop];
}
}
/// Hides the cursor. Part of a balanced call stack.
pub fn hide() {
unsafe {
let _: () = msg_send![class!(NSCursor), hide];
}
}
/// Un-hides the cursor. Part of a balanced call stack.
pub fn unhide() {
unsafe {
let _: () = msg_send![class!(NSCursor), unhide];
}
}
}

View file

@ -21,6 +21,9 @@ pub use alert::Alert;
mod app;
pub use app::*;
mod cursor;
pub use cursor::{Cursor, CursorType};
pub mod menu;
pub mod printing;
pub mod toolbar;

View file

@ -219,6 +219,15 @@ extern fn did_expose<T: WindowDelegate>(this: &Object, _: Sel, _: id) {
window.did_expose();
}
/// Called as part of the responder chain, when, say, the ESC key is hit. If your
/// delegate returns `true` in `should_cancel_on_esc`, then this will allow your
/// window to close when the Esc key is hit. This is mostly useful for Sheet-presented
/// windows, and so the default response from delegates is `false` and must be opted in to.
extern fn cancel<T: WindowDelegate>(this: &Object, _: Sel, _: id) {
let window = load::<T>(this, WINDOW_DELEGATE_PTR);
window.cancel();
}
/// Injects an `NSWindow` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_window_class() -> *const Class {
@ -292,6 +301,7 @@ pub(crate) fn register_window_class_with_delegate<T: WindowDelegate>() -> *const
decl.add_method(sel!(windowDidChangeOcclusionState:), did_change_occlusion_state::<T> as extern fn(&Object, _, _));
decl.add_method(sel!(windowDidExpose:), did_expose::<T> as extern fn(&Object, _, _));
decl.add_method(sel!(windowDidUpdate:), did_update::<T> as extern fn(&Object, _, _));
decl.add_method(sel!(cancelOperation:), cancel::<T> as extern fn (&Object, _, _));
DELEGATE_CLASS = decl.register();
});

View file

@ -52,7 +52,7 @@ impl<T> WindowController<T> where T: WindowDelegate + 'static {
/// Allocates and configures an `NSWindowController` in the Objective-C/Cocoa runtime that maps over
/// to your supplied delegate.
pub fn with(config: WindowConfig, delegate: T) -> Self {
let window = Window::with(config, delegate);
let mut window = Window::with(config, delegate);
let objc = unsafe {
let window_controller_class = register_window_controller_class::<T>();
@ -67,7 +67,7 @@ impl<T> WindowController<T> where T: WindowDelegate + 'static {
ShareId::from_ptr(controller)
};
if let Some(delegate) = &window.delegate {
if let Some(delegate) = &mut window.delegate {
(*delegate).did_load(Window {
delegate: None,
objc: window.objc.clone()

View file

@ -10,10 +10,12 @@
use std::unreachable;
use block::ConcreteBlock;
use core_graphics::base::CGFloat;
use core_graphics::geometry::{CGRect, CGSize};
use objc::{msg_send, sel, sel_impl};
use objc::{msg_send, sel, sel_impl, class};
use objc::runtime::Object;
use objc_id::ShareId;
@ -66,6 +68,11 @@ impl Window {
/// after we initialize the backing `NSWindow`.
pub fn new(config: WindowConfig) -> Window {
let objc = unsafe {
// This behavior might make sense to keep as default (YES), but I think the majority of
// apps that would use this toolkit wouldn't be tab-oriented...
let _: () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing:NO];
let alloc: id = msg_send![register_window_class(), alloc];
// Other types of backing (Retained/NonRetained) are archaic, dating back to the
@ -105,9 +112,14 @@ impl<T> Window<T> where T: WindowDelegate + 'static {
/// enables easier structure of your codebase, and in a way simulates traditional class based
/// architectures... just without the subclassing.
pub fn with(config: WindowConfig, delegate: T) -> Self {
let delegate = Box::new(delegate);
let mut delegate = Box::new(delegate);
let objc = unsafe {
// This behavior might make sense to keep as default (YES), but I think the majority of
// apps that would use this toolkit wouldn't be tab-oriented...
let _: () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing:NO];
let alloc: id = msg_send![register_window_class_with_delegate::<T>(), alloc];
// Other types of backing (Retained/NonRetained) are archaic, dating back to the
@ -143,7 +155,7 @@ impl<T> Window<T> where T: WindowDelegate + 'static {
};
{
&delegate.did_load(Window {
(&mut delegate).did_load(Window {
delegate: None,
objc: objc.clone()
});
@ -447,6 +459,37 @@ impl<T> Window<T> {
scale as f64
}
}
/// Given a window and callback handler, will run it as a "sheet" (model-ish) and then run the
/// handler once the sheet is dismissed.
///
/// This is a bit awkward due to Rust semantics; you have to use the same type of Window as the
/// one you're presenting on, but in practice this isn't too bad since you rarely want a Window
/// without a WindowDelegate.
pub fn begin_sheet<F, W>(&self, window: &Window<W>, completion: F)
where
F: Fn() + Send + Sync + 'static,
W: WindowDelegate + 'static
{
let block = ConcreteBlock::new(move |response: NSInteger| {
completion();
});
let block = block.copy();
unsafe {
let _: () = msg_send![&*self.objc, beginSheet:&*window.objc completionHandler:block];
}
}
/// Closes a sheet.
pub fn end_sheet<W>(&self, window: &Window<W>)
where
W: WindowDelegate + 'static
{
unsafe {
let _: () = msg_send![&*self.objc, endSheet:&*window.objc];
}
}
}
impl<T> Drop for Window<T> {

View file

@ -13,7 +13,7 @@ pub trait WindowDelegate {
/// to set up your views and what not.
///
/// If you're coming from the web, you can think of this as `DOMContentLoaded`.
fn did_load(&self, _window: Window) {}
fn did_load(&mut self, _window: Window) {}
/// Called when the user has attempted to close the window. NOT called when a user quits the
/// application. Return false here if you need to handle the edge case.
@ -118,4 +118,8 @@ pub trait WindowDelegate {
/// Fired when the Window receives an `update` message from higher up in the chain.
fn did_update(&self) {}
/// If you want your window to close when the `ESC` key is hit, implement this.
/// This is mostly useful for windows that present as modal sheets.
fn cancel(&self) {}
}

20
src/progress/enums.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::foundation::{NSUInteger};
/// The type of spinner style you're after.
#[derive(Debug)]
pub enum ProgressIndicatorStyle {
/// A loading bar.
Bar,
/// A spinning circle.
Spinner
}
impl From<ProgressIndicatorStyle> for NSUInteger {
fn from(style: ProgressIndicatorStyle) -> Self {
match style {
ProgressIndicatorStyle::Bar => 0,
ProgressIndicatorStyle::Spinner => 1
}
}
}

45
src/progress/ios.rs Normal file
View file

@ -0,0 +1,45 @@
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, YES, NO, NSUInteger};
use crate::dragdrop::DragInfo;
use crate::view::{VIEW_DELEGATE_PTR, ViewDelegate};
use crate::utils::load;
/// 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_view_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!(UIView);
let mut decl = ClassDecl::new("RSTView", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}
/// Injects an `NSView` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_view_class_with_delegate<T: ViewDelegate>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UIView);
let mut decl = ClassDecl::new("RSTViewWithDelegate", superclass).unwrap();
decl.add_ivar::<usize>(VIEW_DELEGATE_PTR);
VIEW_CLASS = decl.register();
});
unsafe {
VIEW_CLASS
}
}

21
src/progress/macos.rs Normal file
View file

@ -0,0 +1,21 @@
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 }
}

166
src/progress/mod.rs Normal file
View file

@ -0,0 +1,166 @@
use objc_id::ShareId;
use objc::runtime::{Class, Object};
use objc::{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};
mod enums;
pub use enums::ProgressIndicatorStyle;
#[derive(Debug)]
pub struct ProgressIndicator {
/// A pointer to the Objective-C runtime view controller.
pub objc: ShareId<Object>,
/// A pointer to the Objective-C runtime top layout constraint.
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
pub center_y: LayoutAnchorY
}
impl Default for ProgressIndicator {
fn default() -> Self {
ProgressIndicator::new()
}
}
impl ProgressIndicator {
/// Returns a default `ProgressIndicator`, suitable for
pub fn new() -> Self {
let view = unsafe {
let view: id = msg_send![register_progress_indicator_class(), new];
let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
#[cfg(target_os = "macos")]
let _: () = msg_send![view, setWantsLayer:YES];
view
};
ProgressIndicator {
top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
}
}
}
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 {
let _: () = msg_send![&*self.objc, startAnimation:nil];
}
}
pub fn stop_animation(&self) {
unsafe {
let _: () = msg_send![&*self.objc, stopAnimation:nil];
}
}
pub fn increment(&self, by: f64) {
unsafe {
let _: () = msg_send![&*self.objc, incrementBy:by];
}
}
pub fn set_style(&self, style: ProgressIndicatorStyle) {
unsafe {
let style = style as NSUInteger;
let _: () = msg_send![&*self.objc, setStyle:style];
}
}
pub fn set_indeterminate(&self, is_indeterminate: bool) {
unsafe {
let _: () = msg_send![&*self.objc, setIndeterminate:match is_indeterminate {
true => YES,
false => NO
}];
}
}
}
impl Layout for ProgressIndicator {
fn get_backing_node(&self) -> ShareId<Object> {
self.objc.clone()
}
fn add_subview<V: Layout>(&self, view: &V) {
let backing_node = view.get_backing_node();
unsafe {
let _: () = msg_send![&*self.objc, addSubview:backing_node];
}
}
}
impl Drop for ProgressIndicator {
/// 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
/// 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];
}
}
}*/
}
}

View file

@ -23,7 +23,12 @@ pub enum ThumbnailQuality {
/// Ask for them all, and pick which one you
/// use via your provided callback.
All
All,
/// Provided in case this is ever expanded by the OS, and the system
/// returns a thumbnail quality type that can't be matched here. Users
/// could then handle the edge case themselves.
Unknown(NSUInteger)
}
impl From<&ThumbnailQuality> for NSUInteger {
@ -32,11 +37,35 @@ impl From<&ThumbnailQuality> for NSUInteger {
ThumbnailQuality::Icon => 1 << 0,
ThumbnailQuality::Low => 1 << 1,
ThumbnailQuality::High => 1 << 2,
ThumbnailQuality::All => NSUInteger::MAX
ThumbnailQuality::All => NSUInteger::MAX,
ThumbnailQuality::Unknown(x) => *x
}
}
}
impl From<ThumbnailQuality> for NSUInteger {
fn from(quality: ThumbnailQuality) -> Self {
match quality {
ThumbnailQuality::Icon => 1 << 0,
ThumbnailQuality::Low => 1 << 1,
ThumbnailQuality::High => 1 << 2,
ThumbnailQuality::All => NSUInteger::MAX,
ThumbnailQuality::Unknown(x) => x
}
}
}
impl From<NSUInteger> for ThumbnailQuality {
fn from(i: NSUInteger) -> Self {
match i {
0 => ThumbnailQuality::Icon,
2 => ThumbnailQuality::Low,
4 => ThumbnailQuality::High,
NSUInteger::MAX => ThumbnailQuality::All,
i => ThumbnailQuality::Unknown(i)
}
}
}
#[derive(Clone, Debug)]
pub struct ThumbnailConfig {

View file

@ -7,7 +7,7 @@ use block::ConcreteBlock;
use url::Url;
use crate::error::Error;
use crate::foundation::{id, NSUInteger};
use crate::foundation::{id, nil, NSUInteger};
use crate::image::Image;
mod config;
@ -25,13 +25,18 @@ impl ThumbnailGenerator {
pub fn generate<F>(&self, url: &Url, config: ThumbnailConfig, callback: F)
where
//F: Fn(Result<(Image, ThumbnailQuality), Error>) + Send + Sync + 'static
F: Fn(Result<(Image, ThumbnailQuality), Error>) + Send + Sync + 'static
{
let block = ConcreteBlock::new(move |thumbnail: id, thumbnail_type: NSUInteger, error: id| {
unsafe {
let image = Image::with(msg_send![thumbnail, NSImage]);
callback(Ok((image, ThumbnailQuality::Low)));
if error == nil {
unsafe {
let image = Image::with(msg_send![thumbnail, NSImage]);
let quality = ThumbnailQuality::from(thumbnail_type);
callback(Ok((image, ThumbnailQuality::Low)));
}
} else {
let error = Error::new(error);
callback(Err(error));
}
});

View file

@ -20,3 +20,39 @@ impl From<TextAlign> for NSInteger {
}
}
}
/// Instructs text controls how to optimize line breaks.
pub enum LineBreakMode {
/// Wrap at word boundaries (the default)
WrapWords,
/// Wrap at character boundaries
WrapChars,
/// Clip with no regard
Clip,
/// Truncate the start, e.g, ...my sentence
TruncateHead,
/// Truncate the end, e.g, my sentenc...
TruncateTail,
/// Truncate the middle, e.g, my se...ce
TruncateMiddle
}
impl Into<NSUInteger> for LineBreakMode {
fn into(self) -> NSUInteger {
match self {
LineBreakMode::WrapWords => 0,
LineBreakMode::WrapChars => 1,
LineBreakMode::Clip => 2,
LineBreakMode::TruncateHead => 3,
LineBreakMode::TruncateTail => 4,
LineBreakMode::TruncateMiddle => 5
}
}
}

View file

@ -44,10 +44,10 @@ use objc_id::ShareId;
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSInteger, NSString};
use crate::foundation::{id, nil, YES, NO, NSArray, NSInteger, NSUInteger, NSString};
use crate::color::Color;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::text::{Font, TextAlign};
use crate::text::{Font, TextAlign, LineBreakMode};
#[cfg(target_os = "macos")]
mod macos;
@ -237,6 +237,15 @@ impl<T> Label<T> {
let _: () = msg_send![&*self.objc, setFont:&*font.objc];
}
}
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, setLineBreakMode:mode];
}
}
}
impl<T> Layout for Label<T> {

View file

@ -5,7 +5,7 @@ pub mod label;
pub use label::Label;
pub mod enums;
pub use enums::TextAlign;
pub use enums::{LineBreakMode, TextAlign};
pub mod font;
pub use font::Font;