From 14fd4a42b5dd52ee48f214a470efb51251f4fd56 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Fri, 1 Sep 2023 14:07:26 +0200 Subject: [PATCH] Tutorial --- crates/header-translator/src/output.rs | 1 + crates/icrate/Cargo.toml | 28 ++ crates/icrate/examples/tutorial.rs | 323 ++++++++++++++++++++++ crates/icrate/src/__tutorial/chapter_1.md | 41 +++ crates/icrate/src/__tutorial/chapter_2.md | 1 + crates/icrate/src/__tutorial/chapter_3.md | 0 crates/icrate/src/__tutorial/chapter_4.md | 0 crates/icrate/src/__tutorial/intro.md | 9 + crates/icrate/src/__tutorial/mod.rs | 77 ++++++ crates/icrate/src/lib.rs | 17 +- 10 files changed, 491 insertions(+), 6 deletions(-) create mode 100644 crates/icrate/examples/tutorial.rs create mode 100644 crates/icrate/src/__tutorial/chapter_1.md create mode 100644 crates/icrate/src/__tutorial/chapter_2.md create mode 100644 crates/icrate/src/__tutorial/chapter_3.md create mode 100644 crates/icrate/src/__tutorial/chapter_4.md create mode 100644 crates/icrate/src/__tutorial/intro.md create mode 100644 crates/icrate/src/__tutorial/mod.rs diff --git a/crates/header-translator/src/output.rs b/crates/header-translator/src/output.rs index 3ec5622c2..529bb2a33 100644 --- a/crates/header-translator/src/output.rs +++ b/crates/header-translator/src/output.rs @@ -58,6 +58,7 @@ impl Output { "unstable-example-metal".into(), "unstable-example-nspasteboard".into(), "unstable-example-speech_synthesis".into(), + "unstable-example-tutorial".into(), ] .into_iter() .collect(); diff --git a/crates/icrate/Cargo.toml b/crates/icrate/Cargo.toml index b9d9aa0b4..f5e3f801b 100644 --- a/crates/icrate/Cargo.toml +++ b/crates/icrate/Cargo.toml @@ -84,6 +84,12 @@ required-features = [ "unstable-example-metal" ] +[[example]] +name = "tutorial" +required-features = [ + "unstable-example-tutorial" +] + [features] default = ["std", "apple"] @@ -191,6 +197,27 @@ unstable-example-metal = [ "MetalKit_MTKView", "Metal_MTLRenderPassDescriptor", ] +unstable-example-tutorial = [ + "apple", + "AppKit", + "AppKit_NSApplication", + "AppKit_NSButton", + "AppKit_NSLayoutConstraint", + "AppKit_NSLayoutDimension", + "AppKit_NSColor", + "AppKit_NSSlider", + "AppKit_NSStackView", + "AppKit_NSTextField", + "AppKit_NSViewController", + "AppKit_NSWindow", + "AppKit_NSWindowController", + "Foundation", + "Foundation_NSArray", + "Foundation_NSDictionary", + "Foundation_NSNotification", + "Foundation_NSNumber", + "Foundation_NSNumberFormatter", +] # Helps with CI unstable-frameworks-all = ["unstable-frameworks-ios", "unstable-frameworks-macos-13"] @@ -5319,6 +5346,7 @@ unstable-frameworks-macos-10-13 = [ "unstable-example-metal", "unstable-example-nspasteboard", "unstable-example-speech_synthesis", + "unstable-example-tutorial", "unstable-frameworks-macos-10-7", ] unstable-frameworks-macos-10-7 = [ diff --git a/crates/icrate/examples/tutorial.rs b/crates/icrate/examples/tutorial.rs new file mode 100644 index 000000000..381c89741 --- /dev/null +++ b/crates/icrate/examples/tutorial.rs @@ -0,0 +1,323 @@ +#![deny(unsafe_op_in_unsafe_fn)] +use core::cell::RefCell; +use core::ffi::c_void; +use core::ptr::NonNull; + +use icrate::AppKit::{ + NSApplication, NSApplicationActivationPolicyRegular, NSApplicationDelegate, + NSBackingStoreBuffered, NSButton, NSColor, NSControl, NSLayoutAttributeCenterX, + NSLayoutConstraint, NSSlider, NSStackView, NSTextField, + NSUserInterfaceLayoutOrientationVertical, NSView, NSViewController, NSWindow, + NSWindowController, NSWindowStyleMaskClosable, NSWindowStyleMaskMiniaturizable, + NSWindowStyleMaskResizable, NSWindowStyleMaskTitled, +}; +use icrate::Foundation::{ + ns_string, CGFloat, MainThreadMarker, NSArray, NSDictionary, NSNotification, NSNumber, + NSNumberFormatter, NSNumberFormatterNoStyle, NSObject, NSObjectProtocol, NSPoint, NSRect, + NSSize, +}; +use objc2::declare::{Ivar, IvarDrop}; +use objc2::encode::{Encode, Encoding}; +use objc2::rc::{Allocated, Id, WeakId}; +use objc2::runtime::ProtocolObject; +use objc2::{declare_class, extern_methods, msg_send, msg_send_id, mutability, sel, ClassType}; + +declare_class!( + pub struct ViewController { + app_delegate: IvarDrop>, "_app_delegate">, + text_field: IvarDrop, "_text_field">, + slider: IvarDrop, "_slider">, + } + + mod controller_ivars; + + unsafe impl ClassType for ViewController { + type Super = NSViewController; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "ViewController"; + } + + unsafe impl ViewController { + #[method(initWithAppDelegate:)] + unsafe fn __init(this: *mut Self, app_delegate: &AppDelegate) -> Option> { + let this: Option<&mut Self> = unsafe { msg_send![super(this), init] }; + + this.map(|this| { + Ivar::write(&mut this.app_delegate, Box::new(WeakId::from(app_delegate))); + Ivar::write(&mut this.text_field, unsafe { + let view = NSTextField::new(); + view.setFormatter(Some( + { + let formatter = NSNumberFormatter::new(); + formatter.setNumberStyle(NSNumberFormatterNoStyle); + formatter.setMinimum(Some(&NSNumber::new_f32(0.0))); + formatter.setMaximum(Some(&NSNumber::new_f32(10.0))); + formatter + } + .as_super(), + )); + view.widthAnchor() + .constraintGreaterThanOrEqualToConstant(96.0) + .setActive(true); + view.setTarget(Some(&app_delegate)); + view.setAction(Some(sel!(takeFloatValueForVolumeFrom:))); + view + }); + Ivar::write(&mut this.slider, unsafe { + let view = NSSlider::new(); + view.setVertical(true); + view.setMinValue(0.0); + view.setMaxValue(10.0); + view.setAllowsTickMarkValuesOnly(true); + view.setNumberOfTickMarks(11); + view.setContinuous(true); + view.heightAnchor() + .constraintGreaterThanOrEqualToConstant(80.0) + .setActive(true); + view.setTarget(Some(&app_delegate)); + view.setAction(Some(sel!(takeFloatValueForVolumeFrom:))); + view + }); + NonNull::from(this) + }) + } + + #[method(loadView)] + fn load_view(&self) { + let app_delegate = self.app_delegate.load().unwrap(); + + // Unused for now + let view = unsafe { + NSView::initWithFrame( + MainThreadMarker::from(self).alloc(), + NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(100.0, 100.0)), + ) + }; + unsafe { view.setWantsLayer(true) }; + let background = unsafe { NSColor::redColor() }; + unsafe { + // TODO: + // let layer = view.layer(); + // layer.setBorderWidth(2.0); + // layer.setBorderColor(background.cgColor()); + #[repr(transparent)] + struct CGColorRef(*mut c_void); + + unsafe impl Encode for CGColorRef { + const ENCODING: Encoding = Encoding::Pointer(&Encoding::Struct("CGColor", &[])); + } + + let background: CGColorRef = msg_send![&background, CGColor]; + + // CALayer + let layer: Id = msg_send_id![&view, layer]; + let _: () = msg_send![&layer, setBorderWidth: 2.0 as CGFloat]; + let _: () = msg_send![&layer, setBorderColor: background]; + } + // unsafe { self.setView(&view) }; + + let views = [ + Id::into_super(Id::into_super(self.text_field.clone())), + Id::into_super(Id::into_super(self.slider.clone())), + Id::into_super(Id::into_super(unsafe { + let view = NSButton::buttonWithTitle_target_action( + ns_string!("Mute"), + Some(&app_delegate), + Some(sel!(mute:)), + ); + view + })), + ]; + + unsafe { + let stack = NSStackView::stackViewWithViews(&NSArray::from_id_slice(&views)); + stack.setOrientation(NSUserInterfaceLayoutOrientationVertical); + stack.setAlignment(NSLayoutAttributeCenterX); + stack.setSpacing(8.0); + stack.setTranslatesAutoresizingMaskIntoConstraints(false); + + let view = NSView::new(); + view.addSubview(&stack); + + NSLayoutConstraint::activateConstraints( + &NSLayoutConstraint::constraintsWithVisualFormat_options_metrics_views( + ns_string!("|-[stack]-|"), + 0, + None, + &NSDictionary::from_keys_and_objects( + &[ns_string!("stack")], + vec![Id::into_super(Id::into_super(Id::into_super( + Id::into_super(stack.clone()), + )))], + ), + ), + ); + NSLayoutConstraint::activateConstraints( + &NSLayoutConstraint::constraintsWithVisualFormat_options_metrics_views( + ns_string!("V:|-[stack]-|"), + 0, + None, + &NSDictionary::from_keys_and_objects( + &[ns_string!("stack")], + vec![Id::into_super(Id::into_super(Id::into_super( + Id::into_super(stack.clone()), + )))], + ), + ), + ); + + self.setView(&view); + } + } + } +); + +extern_methods!( + unsafe impl ViewController { + #[method_id(initWithAppDelegate:)] + fn init(this: Option>, app_delegate: &AppDelegate) -> Id; + } +); + +impl ViewController { + fn update_user_interface(&self) { + let volume = self.app_delegate.load().unwrap().track.borrow().volume; + unsafe { self.text_field.setFloatValue(volume as _) }; + unsafe { self.slider.setFloatValue(volume as _) }; + } +} + +declare_class!( + pub struct AppDelegate { + track: IvarDrop>, "_track">, + window_controller: + IvarDrop>>>, "_window_controller">, + view_controller: IvarDrop>>>, "_view_controller">, + } + + mod ivars; + + unsafe impl ClassType for AppDelegate { + type Super = NSObject; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "AppDelegate"; + } + + unsafe impl AppDelegate { + #[method(init)] + unsafe fn __init(this: *mut Self) -> Option> { + let this: Option<&mut Self> = unsafe { msg_send![super(this), init] }; + + this.map(|this| { + Ivar::write(&mut this.track, Box::new(RefCell::new(Track::default()))); + Ivar::write(&mut this.window_controller, Box::new(RefCell::new(None))); + Ivar::write(&mut this.view_controller, Box::new(RefCell::new(None))); + NonNull::from(this) + }) + } + + #[method(mute:)] + fn mute(&self, _sender: &NSButton) { + self.track.borrow_mut().volume = 0.0; + + self.view_controller + .borrow() + .as_ref() + .unwrap() + .update_user_interface(); + } + + #[method(takeFloatValueForVolumeFrom:)] + fn take_float_value_for_volume_from(&self, sender: &NSControl) { + let new_value = unsafe { sender.floatValue() } as _; + + self.track.borrow_mut().volume = new_value; + + self.view_controller + .borrow() + .as_ref() + .unwrap() + .update_user_interface(); + } + } + + unsafe impl NSApplicationDelegate for AppDelegate { + #[method(applicationDidFinishLaunching:)] + fn did_finish_launching(&self, notification: &NSNotification) { + let app: Id = unsafe { Id::cast(notification.object().unwrap()) }; + let mtm = MainThreadMarker::from(self); + // Or just let app = NSApplication::sharedApplication(); + unsafe { + let window = NSWindow::initWithContentRect_styleMask_backing_defer( + mtm.alloc(), + NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(480.0, 270.0)), + NSWindowStyleMaskTitled + | NSWindowStyleMaskClosable + | NSWindowStyleMaskMiniaturizable + | NSWindowStyleMaskResizable, + NSBackingStoreBuffered, + false, + ); + // TODO: Make this the default when creating new windows, to + // preserve memory safety! + window.setReleasedWhenClosed(false); + + window.center(); + window.setTitle(ns_string!("A window")); + + let view_controller = ViewController::init(mtm.alloc(), self); + window.setContentView(Some(&view_controller.view())); + + let window_controller = + NSWindowController::initWithWindow(mtm.alloc(), Some(&window)); + window_controller.setWindowFrameAutosaveName(ns_string!("main")); + + window.makeKeyAndOrderFront(None); + + app.setActivationPolicy(NSApplicationActivationPolicyRegular); + app.activateIgnoringOtherApps(true); + + *self.window_controller.borrow_mut() = Some(window_controller); + *self.view_controller.borrow_mut() = Some(view_controller); + + self.view_controller + .borrow() + .as_ref() + .unwrap() + .update_user_interface(); + } + } + } +); + +unsafe impl NSObjectProtocol for AppDelegate {} + +extern_methods!( + unsafe impl AppDelegate { + #[method_id(init)] + fn init(this: Option>) -> Id; + } +); + +#[derive(Debug)] +pub struct Track { + volume: f32, +} + +impl Default for Track { + fn default() -> Self { + Self { volume: 5.0 } + } +} + +fn main() { + let mtm = MainThreadMarker::new().unwrap(); + + unsafe { + let app = NSApplication::sharedApplication(); + let delegate = AppDelegate::init(mtm.alloc()); + app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + + app.run(); + } +} diff --git a/crates/icrate/src/__tutorial/chapter_1.md b/crates/icrate/src/__tutorial/chapter_1.md new file mode 100644 index 000000000..128f4f329 --- /dev/null +++ b/crates/icrate/src/__tutorial/chapter_1.md @@ -0,0 +1,41 @@ +# Chapter 1 - What is Objective-C? + +Objective-C is a superset of C that has been the standard programming language on Apple platforms like macOS, iOS, iPadOS, tvOS and watchOS for many years. While it has since been superseded by Swift, most of the core interfaces, libraries and frameworks that are in use on Apple systems are still written in Objective-C, and hence it is still useful for us to learn a bit about it. + +Objective-C provides object-oriented capabilities, that work similarly to other object-oriented languages; objects contain variables and methods, as defined by classes, and the methods are customizable by subclasses. + +Additionally, everything in Objective-C is dynamic, allowing objects to be introspected and customized at runtime (as an example, it is possible, although discouraged, to change the methods of an object at runtime). + + +## Reference-counting + +Every object in Objective-C is allocated and stored on the heap, and as such, is only accesible behind pointers. To make sharing and ownership easier, ... + +means that objects are not accesible directly, but instead must either be held as `&NSObject` or `Id` + +`rc::Id` / reference-counting / `ClassType::retain` + + +## `Deref` and superclasses + +Objective-C (and Swift) has single parent inheritance, which means that every class has a superclass, except for the few "root" classes like `NSObject` and `NSProxy`. + +"instance" is used to "class" is used to describe the thing that "instances" are an instance of. + +Rust, however, has no notion of inheritance, so to avoid . This may be slightly confusing sometimes, but was considered to be the best option amongst evils. + +The documentation should show the methods available on a class, even through it's `Deref`-chain. The `Deref` / superclass chain always ends at `runtime::Object`, so that it is easily possible to call methods which accept any object. + +```rust +fn my_method(obj: &NSObject) { + // Do something with the object +} + +let string: &NSString; +// Because of the `&`, deref-coercion kicks in, and the string is converted to +// `&NSObject` before being passed to the method. +my_method(&string); +``` + +This only works in select cases, though, so sometimes you will have to explicitly get the superclass of the object, with either (possibly repeated) calls to `Id::into_super` or to `ClassType::as_super`. + diff --git a/crates/icrate/src/__tutorial/chapter_2.md b/crates/icrate/src/__tutorial/chapter_2.md new file mode 100644 index 000000000..f4a0f5ad5 --- /dev/null +++ b/crates/icrate/src/__tutorial/chapter_2.md @@ -0,0 +1 @@ +# Chapter 2 - ... diff --git a/crates/icrate/src/__tutorial/chapter_3.md b/crates/icrate/src/__tutorial/chapter_3.md new file mode 100644 index 000000000..e69de29bb diff --git a/crates/icrate/src/__tutorial/chapter_4.md b/crates/icrate/src/__tutorial/chapter_4.md new file mode 100644 index 000000000..e69de29bb diff --git a/crates/icrate/src/__tutorial/intro.md b/crates/icrate/src/__tutorial/intro.md new file mode 100644 index 000000000..211ff77f2 --- /dev/null +++ b/crates/icrate/src/__tutorial/intro.md @@ -0,0 +1,9 @@ +# Tutorial + +This tutorial assumes that you are already familiar with Rust, and are on a system running macOS. This tutorial will walk you through recreating [a basic Cocoa application][first-mac-app], and while prior experience with Objective-C or Swift is not required, there are quite a few concepts involved with Cocoa development that will be glossed over. + +Hopefully though, you should be able to grasp the basics of `icrate` once you're done - the rest is "just" about learning the specific framework APIs. + +With that out of the way, let's get started ... + +[first-mac-app]: https://developer.apple.com/library/archive/referencelibrary/GettingStarted/RoadMapOSX/books/RM_YourFirstApp_Mac/Articles/Introduction.html diff --git a/crates/icrate/src/__tutorial/mod.rs b/crates/icrate/src/__tutorial/mod.rs new file mode 100644 index 000000000..5f78c15a2 --- /dev/null +++ b/crates/icrate/src/__tutorial/mod.rs @@ -0,0 +1,77 @@ +//! TODO +#![doc = include_str!("./intro.md")] + +pub mod chapter_1 { + #![doc = include_str!("./chapter_1.md")] + pub use super::chapter_2 as next; +} + +pub mod chapter_2 { + #![doc = include_str!("./chapter_2.md")] + pub use super::chapter_1 as prev; + pub use super::chapter_3 as next; +} + +pub mod chapter_3 { + #![doc = include_str!("./chapter_3.md")] + pub use super::chapter_2 as prev; + pub use super::chapter_4 as next; +} + +pub mod chapter_4 { + #![doc = include_str!("./chapter_4.md")] + pub use super::chapter_3 as prev; +} + +/* +How is things translated? +- Class -> Opaque struct + - Pointer to class -> Option<&MyClass> or &MyClass +- Protocol -> Trait +- Property getter -> Method +- Property setter -> Method +- Class method -> Associated function +- Instance method -> Method + - Properties vs. methods + +Object creation + +Mutability/ownership: +- ... +- `ClassType::alloc` / init methods vs. `new` methods +- `rc::Allocated` +- `MainThreadMarker`/`MainThreadMarker::alloc` + +Declaring classes yourself: +- "Some of these objects are immediately usable, such as basic data types like strings and numbers, or user interface elements like buttons and table views. Some are designed for you to customize with your own code to behave in the way you require. The app development process involves deciding how best to customize and combine the objects provided by the underlying frameworks with your own objects to give your app its unique set of features and functionality." +- `ClassType` vs. `Message` +- Instance variables +- Name must be unique +- Methods: + - New methods + - Overriding methods + - Implementing protocols +- `declare_class!` + +Autoreleased objects: +- `rc::autoreleasepool` +- Autoreleasing in declared classes + +Advanced topics: +- `rc::WeakId` and `retain`ing vs copying +- `runtime::Class` +- `runtime::ProtocolObject` +- Type-encodings +- Exceptions??? +- Message sending +- "literals" / ns_string! macro (and others in the future). + + +What now? + +If you want to learn more about Objective-C and Swift first, some great starting points are the [Programming with Objective-C][objc-book] book and [The Swift Programming Language][swift-book] book. + +[objc-book]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html +[swift-book]: https://docs.swift.org/swift-book/documentation/the-swift-programming-language + +*/ diff --git a/crates/icrate/src/lib.rs b/crates/icrate/src/lib.rs index e4e6444a3..0596d0f0d 100644 --- a/crates/icrate/src/lib.rs +++ b/crates/icrate/src/lib.rs @@ -1,12 +1,15 @@ //! # Bindings to Apple's frameworks //! -//! `icrate` is an autogenerated interface to Apple's Objective-C frameworks -//! like AppKit, Foundation, Metal, WebKit, and so on. +//! `icrate` is an interface to Apple's Objective-C frameworks like +//! AppKit, Foundation, Metal, WebKit, and so on. //! -//! The bindings currently contain very little documentation, you should view -//! [Apple's developer documentation][apple-doc-index] for detailed -//! information about each API. (There are [plans][#309] for importing that -//! documentation here). +//! Quick links: +//! - [Tutorial][self::__tutorial] +//! +//! The bindings are mostly automatically generated, and currently contain +//! very little documentation, you should view [Apple's developer +//! documentation][apple-doc-index] for detailed information about each API. +//! (There are [plans][#309] for importing that documentation here). //! //! [#309]: https://github.com/madsmtm/objc2/issues/309 //! [apple-doc-index]: https://developer.apple.com/documentation/technologies @@ -133,6 +136,8 @@ pub extern crate block2; mod common; #[macro_use] mod macros; +#[cfg(any(doc, doctest))] +pub mod __tutorial; #[allow(unreachable_pub)] #[allow(unused_imports)] #[allow(deprecated)]