Skip to content

Commit 4718f38

Browse files
committed
Add unfinished doc-viewer example
1 parent 0dd51d5 commit 4718f38

File tree

9 files changed

+848
-1
lines changed

9 files changed

+848
-1
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ jobs:
113113
--exclude header-translator --exclude test-assembly
114114
--exclude test-ui --exclude test-fuzz --exclude tests
115115
--exclude objc2-exception-helper --exclude apple-doc
116+
--exclude doc-viewer
116117
- name: iOS ARMv7s
117118
target: armv7s-apple-ios
118119
build-std: true
@@ -123,7 +124,7 @@ jobs:
123124
--exclude header-translator --exclude test-assembly
124125
--exclude test-ui --exclude test-fuzz --exclude tests
125126
--exclude objc2-exception-helper --exclude apple-doc
126-
--exclude objc2-io-usb-host
127+
--exclude doc-viewer --exclude objc2-io-usb-host
127128
- name: visionOS Aarch64 simulator
128129
target: aarch64-apple-visionos-sim
129130
build-std: true
@@ -133,6 +134,7 @@ jobs:
133134
--exclude header-translator --exclude test-assembly
134135
--exclude test-ui --exclude test-fuzz --exclude tests
135136
--exclude objc2-exception-helper --exclude apple-doc
137+
--exclude doc-viewer
136138
137139
- name: GNUStep + exceptions
138140
target: x86_64-unknown-linux-gnu

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/doc-viewer/Cargo.toml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
[package]
2+
name = "doc-viewer"
3+
version = "0.0.0"
4+
edition = "2021"
5+
license = "Zlib OR Apache-2.0 OR MIT"
6+
publish = false
7+
8+
# Make the `src/main.rs` an example instead.
9+
autobins = false
10+
[[example]]
11+
name = "doc-viewer"
12+
path = "src/main.rs"
13+
14+
[target.'cfg(target_os = "macos")'.dependencies]
15+
block2 = "0.6.1"
16+
objc2 = "0.6.2"
17+
objc2-core-foundation = { version = "0.3.1", default-features = false, features = ["CFCGTypes"] }
18+
objc2-foundation = { version = "0.3.1", default-features = false, features = [
19+
"std",
20+
"block2",
21+
"NSString",
22+
"NSGeometry",
23+
"NSNotification",
24+
"NSURL",
25+
"NSURLRequest",
26+
"NSThread",
27+
] }
28+
objc2-app-kit = { version = "0.3.1", default-features = false, features = [
29+
"std",
30+
"NSResponder",
31+
"NSView",
32+
"NSColor",
33+
"NSButton",
34+
"NSButtonCell",
35+
"NSMenu",
36+
"NSMenuItem",
37+
"NSStackView",
38+
"NSText",
39+
"NSTextField",
40+
"NSTextView",
41+
"NSWindow",
42+
"NSControl",
43+
"NSApplication",
44+
"NSRunningApplication",
45+
"NSGraphics",
46+
"NSLayoutConstraint",
47+
"NSUserInterfaceLayout",
48+
"default", # TEMPORARY
49+
] }
50+
51+
# Depend on the `apple-doc` crate for the documentation data we display.
52+
#
53+
# In a real-world application, you probably don't want this dependency,
54+
# but rather take your data from somewhere else.
55+
apple-doc = { path = "../../crates/apple-doc" }
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
use std::cell::OnceCell;
2+
3+
use objc2::rc::Retained;
4+
use objc2::runtime::ProtocolObject;
5+
use objc2::{define_class, msg_send, sel, Ivars, MainThreadMarker, MainThreadOnly};
6+
use objc2_app_kit::{
7+
NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSMenu, NSMenuItem,
8+
NSWindow, NSWindowController, NSWindowStyleMask,
9+
};
10+
use objc2_foundation::{ns_string, NSNotification, NSObject, NSObjectProtocol};
11+
12+
use crate::split_view_controller::SplitViewController;
13+
14+
define_class!(
15+
// SAFETY:
16+
// - The superclass NSObject does not have any subclassing requirements.
17+
// - `AppDelegate` does not implement `Drop`.
18+
#[unsafe(super(NSObject))]
19+
#[thread_kind = MainThreadOnly]
20+
pub struct AppDelegate {
21+
window: OnceCell<Retained<NSWindow>>,
22+
window_controller: OnceCell<Retained<NSWindowController>>,
23+
}
24+
25+
// SAFETY: No problematic methods on `NSObjectProtocol` are implemented.
26+
unsafe impl NSObjectProtocol for AppDelegate {}
27+
28+
// SAFETY: `NSApplicationDelegate` has no safety requirements.
29+
unsafe impl NSApplicationDelegate for AppDelegate {
30+
// SAFETY: The signature is correct.
31+
#[unsafe(method(applicationDidFinishLaunching:))]
32+
fn did_finish_launching(&self, notification: &NSNotification) {
33+
let mtm = self.mtm();
34+
let app = notification
35+
.object()
36+
.unwrap()
37+
.downcast::<NSApplication>()
38+
.unwrap();
39+
40+
add_menubar(&app);
41+
42+
let view_controller = SplitViewController::new(mtm);
43+
44+
let window = NSWindow::windowWithContentViewController(&view_controller);
45+
46+
// The view controller is a split view, and we'd like it to take
47+
// up the entire left side of the window.
48+
window.setStyleMask(window.styleMask() | NSWindowStyleMask::FullSizeContentView);
49+
50+
let window_controller =
51+
NSWindowController::initWithWindow(NSWindowController::alloc(mtm), Some(&window));
52+
53+
unsafe { window_controller.showWindow(None) };
54+
// TODO: Enable once we've figured out the layout.
55+
// window_controller.setWindowFrameAutosaveName(ns_string!("MainWindow"));
56+
57+
// Since we're compiling with Cargo, and not bundling the binary
58+
// into an `.app`, we need to change the activation policy and
59+
// activate the application such that our window will appear.
60+
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
61+
#[allow(deprecated)]
62+
app.activateIgnoringOtherApps(false);
63+
64+
// Store for later use.
65+
self.window().set(window).unwrap();
66+
self.window_controller().set(window_controller).unwrap();
67+
}
68+
69+
// SAFETY: The signature is correct.
70+
#[unsafe(method(applicationWillTerminate:))]
71+
fn will_terminate(&self, _notification: &NSNotification) {}
72+
}
73+
);
74+
75+
impl AppDelegate {
76+
// FIXME: Make it possible to avoid this boilerplate.
77+
fn new(mtm: MainThreadMarker) -> Retained<Self> {
78+
let this = Self::alloc(mtm);
79+
let this = this.set_ivars(Ivars::<Self> {
80+
window: Default::default(),
81+
window_controller: Default::default(),
82+
});
83+
// SAFETY: `AppDelegate` is safe to initialize.
84+
unsafe { msg_send![super(this), init] }
85+
}
86+
}
87+
88+
pub fn set_application_delegate(app: &NSApplication) {
89+
let delegate = AppDelegate::new(app.mtm());
90+
let object = ProtocolObject::from_ref(&*delegate);
91+
app.setDelegate(Some(object));
92+
}
93+
94+
/// Create a minimal menubar with a "Quit" entry.
95+
fn add_menubar(app: &NSApplication) {
96+
let mtm = app.mtm();
97+
98+
let menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), ns_string!(""));
99+
let menu_app_item = unsafe {
100+
NSMenuItem::initWithTitle_action_keyEquivalent(
101+
NSMenuItem::alloc(mtm),
102+
ns_string!(""),
103+
None,
104+
ns_string!(""),
105+
)
106+
};
107+
let menu_app_menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), ns_string!(""));
108+
unsafe {
109+
menu_app_menu.addItemWithTitle_action_keyEquivalent(
110+
ns_string!("Quit"),
111+
Some(sel!(terminate:)),
112+
ns_string!("q"),
113+
)
114+
};
115+
menu_app_item.setSubmenu(Some(&menu_app_menu));
116+
menu.addItem(&menu_app_item);
117+
118+
app.setMainMenu(Some(&menu));
119+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use std::cell::RefCell;
2+
3+
use apple_doc::{external_dir, url_to_uuid, BlobStore, Lmdb, NavigatorItem, SqliteDb};
4+
use objc2::rc::Retained;
5+
use objc2::{define_class, msg_send, Ivars, MainThreadMarker, MainThreadOnly};
6+
use objc2_app_kit::{NSResponder, NSViewController};
7+
use objc2_foundation::NSObject;
8+
9+
define_class!(
10+
// SAFETY:
11+
// - We correctly override `NSViewController` methods.
12+
// - `SplitViewController` does not implement `Drop`.
13+
#[unsafe(super(NSViewController, NSResponder, NSObject))]
14+
pub struct DetailViewController {
15+
sqlite_db: SqliteDb,
16+
blobs: RefCell<BlobStore>,
17+
lmdb: Lmdb,
18+
}
19+
20+
impl DetailViewController {
21+
// SAFETY: The signature is correct.
22+
#[unsafe(method(viewDidLoad))]
23+
fn view_did_load(&self) {
24+
let _: () = unsafe { msg_send![super(self), viewDidLoad] };
25+
let _mtm = self.mtm();
26+
27+
// TODO
28+
}
29+
}
30+
);
31+
32+
impl DetailViewController {
33+
pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
34+
let external_dir = external_dir();
35+
36+
let sqlite_db = apple_doc::SqliteDb::from_external_dir(&external_dir).unwrap();
37+
let blobs = RefCell::new(
38+
apple_doc::BlobStore::from_external_dir(&sqlite_db, &external_dir).unwrap(),
39+
);
40+
41+
let lmdb = Lmdb::from_external_dir(&external_dir).unwrap();
42+
43+
let this = Self::alloc(mtm).set_ivars(Ivars::<Self> {
44+
sqlite_db,
45+
blobs,
46+
lmdb,
47+
});
48+
// SAFETY: `DetailViewController` is safe to initialize.
49+
unsafe { msg_send![super(this), init] }
50+
}
51+
52+
pub fn switch_to(&self, navigator_id: u32, item: &NavigatorItem<'_>) {
53+
let url = self.lmdb().url_from_navigator_id(navigator_id).unwrap();
54+
55+
// The type of this depends on the kind of item.
56+
//
57+
// - `Kind::Module`: The underlying framework/module name
58+
// (Core Animation -> QuartzCore).
59+
// - `Kind::Article`/SampleCode: Name is unique?
60+
//
61+
// - `Kind::Class`: `c:objc(cs)CALayer`.
62+
// - `Kind::Protocol`: `c:objc(pl)CALayerDelegate`.
63+
// - `Kind::Method`: `c:objc(pl)CAAction(im)runActionForKey:object:arguments:`.
64+
// - `Kind::Method`: `c:objc(pl)CAAction(im)runActionForKey:object:arguments:`.
65+
66+
dbg!((url, item, item.kind));
67+
68+
if let Some(url) = url {
69+
let uuid = url_to_uuid(url);
70+
if let Some(r) = self.sqlite_db().get_ref(&uuid).unwrap() {
71+
let doc = self.blobs().borrow_mut().parse_doc(&r).unwrap();
72+
dbg!(doc);
73+
} else {
74+
eprintln!("invalid ID: {uuid:?} in {url:?}");
75+
return;
76+
}
77+
}
78+
}
79+
}

examples/doc-viewer/src/main.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//! A re-implementation of Xcode's Developer Documentation viewer.
2+
//!
3+
//! This uses the implementation in `crates/apple-doc` to get the data from
4+
//! Xcode's directories.
5+
//!
6+
//! This is unfinished, but should give a bit of an idea of what's possible.
7+
//!
8+
//! TODO:
9+
//! - https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaBindings/CocoaBindings.html
10+
//! - https://developer.apple.com/documentation/appkit/navigating-hierarchical-data-using-outline-and-split-views?language=objc
11+
#![allow(non_snake_case)]
12+
13+
#[cfg(target_os = "macos")]
14+
mod app_delegate;
15+
#[cfg(target_os = "macos")]
16+
mod detail_view_controller;
17+
#[cfg(target_os = "macos")]
18+
mod navigator;
19+
#[cfg(target_os = "macos")]
20+
mod split_view_controller;
21+
22+
#[cfg(target_os = "macos")]
23+
fn main() {
24+
let mtm = objc2::MainThreadMarker::new().unwrap();
25+
objc2::rc::autoreleasepool(|_| {
26+
let app = objc2_app_kit::NSApplication::sharedApplication(mtm);
27+
app_delegate::set_application_delegate(&app);
28+
app.run();
29+
});
30+
}
31+
32+
#[cfg(not(target_os = "macos"))]
33+
fn main() {
34+
panic!("unsupported platform in this example");
35+
}

examples/doc-viewer/src/module.svg

Lines changed: 29 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)