Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
- 'crates/rift-protocol/src/**'
- 'rift.default.toml'
- 'src/bin/rift-cli.rs'
- 'src/common/config.rs'
- 'src/common/config/**'

permissions:
contents: read
Expand Down
32 changes: 20 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 12 additions & 26 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2024"
build = "build.rs"

[workspace]
members = [".", "crates/rift-client", "crates/rift-protocol"]
members = [".", "crates/rift-client", "crates/rift-protocol", "crates/cgs"]
resolver = "3"

[[bin]]
Expand All @@ -14,12 +14,12 @@ test = false

[profile.dev]
opt-level = 0
debug = 1
debug = "line-tables-only"
incremental = true
codegen-units = 16
codegen-units = 256
debug-assertions = true
overflow-checks = true
split-debuginfo = "packed"
split-debuginfo = "unpacked"
panic = "unwind"

[profile.release]
Expand Down Expand Up @@ -53,6 +53,7 @@ custom-event-loop = ["objc2-foundation/NSDate", "objc2-foundation/NSRunLoop"]
disable-axuielement-destroyed = []

[dependencies]
cgs = { path = "crates/cgs" }
anyhow = { version = "1.0.83", default-features = false }
arc-swap = "1"
ascii_tree = "0.1.1"
Expand All @@ -66,39 +67,26 @@ nix = { version = "0.30.1", features = ["process", "user"] }
objc2 = { version = "0.6.2", default-features = false, features = ["exception"] }
objc2-app-kit = { version = "0.3.2", default-features = false, features = [
"NSAlert",
"NSAppearance",
"NSApplication",
"NSAttributedString",
"NSButton",
"NSColor",
"NSControl",
"NSCell",
"NSColorSpace",
"NSCursor",
"NSEvent",
"NSFont",
"NSFontDescriptor",
"NSGraphics",
"NSGraphicsContext",
"NSImage",
"NSImageRep",
"NSMenu",
"NSMenuItem",
"NSOpenPanel",
"NSPanel",
"NSStringDrawing",
"NSResponder",
"NSRunningApplication",
"NSSavePanel",
"NSScreen",
"NSStatusBar",
"NSStatusBarButton",
"NSStatusItem",
"NSTouch",
"NSView",
"NSWindow",
"NSWorkspace",
"NSTextField",
"NSText",
"libc",
"objc2-core-foundation",
"objc2-core-graphics",
Expand Down Expand Up @@ -166,17 +154,10 @@ objc2-quartz-core = { version = "0.3.2", default-features = false, features = [
"CADisplayLink",
"CAFrameRateRange",
"CABase",
"CAEAGLLayer",
"CAEDRMetadata",
"CAGradientLayer",
"CAMediaTimingFunction",
"CAOpenGLLayer",
"CARenderer",
"CATextLayer",
"CATransaction",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-metal"
]}
once_cell = { version = "1.21.3", default-features = false }
parking_lot = "0.12"
Expand All @@ -189,6 +170,8 @@ static_assertions = "1.1.0"
tokio = { version = "1.35.1", features = ["libc", "macros"] }
tokio-stream = "0.1.16"
tokio-util = "0.7.15"
toml_edit = { version = "0.23.10", features = ["serde"] }
tempfile = { version = "3.17.1", default-features = false }
toml = { version = "0.9.2", features = ["display", "parse", "serde"], default-features = false }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["std", "tracing", "env-filter"], default-features = false }
Expand All @@ -215,4 +198,7 @@ pretty_assertions = "1.4.0"
tokio = { version = "1.35.1", features = ["sync", "tokio-macros"] }
test-log = { version = "0.2.16", default-features = false }
test_bin = "0.5.0"
tempfile = { version = "3.17.1", default-features = false }

[[test]]
name = "settings_native"
harness = false
25 changes: 25 additions & 0 deletions crates/cgs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "cgs"
version = "0.1.1"
description = "a ui library for macos"
license = "Apache-2.0"
edition = "2024"

[dependencies]
objc2 = { version = "0.6.2", default-features = false, features = ["std"] }
block2 = { version = "0.6.2", default-features = false, features = ["alloc"] }
objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSArray", "NSDictionary", "NSString", "NSNumberFormatter", "NSFormatter", "NSValue", "NSKeyValueCoding", "NSNotification", "NSIndexSet", "NSURL", "NSAttributedString", "NSGeometry", "NSTimer", "NSDate"] }
objc2-core-foundation = { version = "0.3.2", default-features = false, features = ["CFString", "CFNumber"] }
objc2-core-graphics = { version = "0.3.2", default-features = false, features = ["CGContext", "CGColorSpace", "CGError"] }
objc2-quartz-core = { version = "0.3.2", default-features = false, features = ["CALayer", "CATransaction", "objc2-core-foundation", "objc2-core-graphics"] }
objc2-app-kit = { version = "0.3.2", default-features = false, features = [
"std", "NSAttributedString", "NSAccessibility", "NSAccessibilityProtocols", "NSAccessibilityConstants", "NSApplication", "NSAnimation", "NSAnimationContext", "NSRunningApplication", "NSAppearance", "NSWindow", "NSPanel", "NSView", "NSViewController", "NSLayoutAnchor", "NSLayoutGuide", "NSLayoutConstraint", "NSStackView", "NSGridView", "NSScrollView", "NSClipView", "NSScroller", "NSSplitView", "NSSplitViewController", "NSSplitViewItem", "NSBox", "NSVisualEffectView", "NSGlassEffectView", "NSControl", "NSCell", "NSActionCell", "NSButton", "NSButtonCell", "NSSwitch", "NSSegmentedControl", "NSSegmentedCell", "NSPopUpButton", "NSPopUpButtonCell", "NSComboBox", "NSTextField", "NSTextFieldCell", "NSText", "NSTextView", "NSTextContainer", "NSSearchField", "NSTokenField", "NSSlider", "NSSliderCell", "NSStepper", "NSColor", "NSColorWell", "NSFont", "NSFontDescriptor", "NSImage", "NSImageView", "NSMenu", "NSMenuItem", "NSMenuItemBadge", "NSMenuItemCell", "NSStatusBar", "NSStatusItem", "NSStatusBarButton", "NSTableView", "NSTableColumn", "NSTableHeaderView", "NSTableCellView", "NSOutlineView", "NSDragging", "NSDraggingSession", "NSPasteboard", "NSPasteboardItem", "NSPopover", "NSAlert", "NSOpenPanel", "NSSavePanel", "NSToolbar", "NSToolbarItem", "NSProgressIndicator", "NSEvent", "NSResponder", "NSGraphicsContext", "NSGraphics", "NSUserInterfaceLayout", "NSUserInterfaceValidation", "NSUserInterfaceItemIdentification", "NSWorkspace", "block2", "objc2-core-foundation", "objc2-core-graphics", "objc2-quartz-core"
] }
tracing = { version = "0.1.40", default-features = false }

[[test]]
name = "native"
harness = false

[dev-dependencies]
objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSDraggingItem"] }
3 changes: 3 additions & 0 deletions crates/cgs/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
println!("cargo:rustc-link-search=framework=/System/Library/PrivateFrameworks");
}
3 changes: 3 additions & 0 deletions crates/cgs/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# cgs

> cgs aka core graphics
16 changes: 16 additions & 0 deletions crates/cgs/src/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use objc2::rc::Retained;
use objc2_app_kit::NSApplication;

use crate::Ui;
pub struct Application(Retained<NSApplication>);
impl Application {
pub fn shared(ui: &Ui) -> Self { Self(NSApplication::sharedApplication(ui.mtm())) }

pub fn ns_application(&self) -> &NSApplication { &self.0 }

pub fn activate(&self) { self.0.activate(); }

pub fn hide(&self) { self.0.hide(None); }

pub fn is_active(&self) -> bool { self.0.isActive() }
}
121 changes: 121 additions & 0 deletions crates/cgs/src/bridge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::cell::RefCell;

use objc2::rc::Retained;
use objc2::runtime::{AnyObject, NSObjectProtocol, ProtocolObject};
use objc2::{DefinedClass, MainThreadOnly, Message, define_class, msg_send, sel};
use objc2_app_kit::{
NSControl, NSControlTextEditingDelegate, NSMenu, NSMenuDelegate, NSPopoverDelegate,
NSSearchFieldDelegate, NSTextDelegate, NSTextFieldDelegate, NSTextViewDelegate,
NSTokenFieldDelegate, NSWindow, NSWindowDelegate,
};
use objc2_foundation::{NSNotification, NSObject};

use crate::Ui;

pub(crate) fn callback(f: impl FnOnce()) {
// Rust unwinding must never cross an Objective-C callback boundary.
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).is_err() {
tracing::error!("panic in native UI callback");
}
}
type Action = Box<dyn FnMut(&AnyObject)>;
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "CgUiActionTarget"]
#[ivars = RefCell<Option<Action>>]
pub(crate) struct ActionTarget;
unsafe impl NSObjectProtocol for ActionTarget {}
impl ActionTarget {
#[unsafe(method(invoke:))]
fn invoke(&self, sender: &AnyObject) {
let _keep_alive = self.retain();
if let Ok(mut cb) = self.ivars().try_borrow_mut() {
if let Some(cb) = cb.as_mut() { callback(|| cb(sender)); }
}
}
}
);
impl ActionTarget {
pub(crate) fn new(ui: &Ui) -> Retained<Self> {
let this = Self::alloc(ui.mtm()).set_ivars(RefCell::new(None));
unsafe { msg_send![super(this), init] }
}

pub(crate) fn set(&self, f: impl FnMut(&AnyObject) + 'static) {
*self.ivars().borrow_mut() = Some(Box::new(f));
}

pub(crate) fn attach(&self, control: &NSControl) {
unsafe {
control.setTarget(Some(self));
control.setAction(Some(sel!(invoke:)));
}
}
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Event {
TextChanged,
TextCommitted,
MenuOpened,
MenuClosed,
WindowClosed,
PopoverClosed,
}
type EventCallback = Box<dyn FnMut(Event, &AnyObject)>;
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "CgUiDelegateBridge"]
#[ivars = RefCell<Option<EventCallback>>]
pub(crate) struct DelegateBridge;
unsafe impl NSObjectProtocol for DelegateBridge {}
unsafe impl NSControlTextEditingDelegate for DelegateBridge {
#[unsafe(method(controlTextDidChange:))]
fn changed(&self, note: &NSNotification) { if let Some(sender) = note.object() { self.emit(Event::TextChanged, &sender); } }
#[unsafe(method(controlTextDidEndEditing:))]
fn committed(&self, note: &NSNotification) { if let Some(sender) = note.object() { self.emit(Event::TextCommitted, &sender); } }
}
unsafe impl NSTextDelegate for DelegateBridge {
#[unsafe(method(textDidChange:))]
fn text_changed(&self,note:&NSNotification){if let Some(sender)=note.object(){self.emit(Event::TextChanged,&sender);}}
}
unsafe impl NSTextViewDelegate for DelegateBridge {}
unsafe impl NSTextFieldDelegate for DelegateBridge {}
unsafe impl NSSearchFieldDelegate for DelegateBridge {}
unsafe impl NSTokenFieldDelegate for DelegateBridge {}
unsafe impl NSMenuDelegate for DelegateBridge {
#[unsafe(method(menuWillOpen:))]
fn menu_open(&self, menu: &NSMenu) { self.emit(Event::MenuOpened, menu); }
#[unsafe(method(menuDidClose:))]
fn menu_close(&self, menu: &NSMenu) { self.emit(Event::MenuClosed, menu); }
}
unsafe impl NSWindowDelegate for DelegateBridge {
#[unsafe(method(windowWillClose:))]
fn window_close(&self, note: &NSNotification) { if let Some(sender) = note.object() { self.emit(Event::WindowClosed, &sender); } }
}
unsafe impl NSPopoverDelegate for DelegateBridge {
#[unsafe(method(popoverDidClose:))]
fn popover_close(&self, note: &NSNotification) { if let Some(sender) = note.object() { self.emit(Event::PopoverClosed, &sender); } }
}
);
impl DelegateBridge {
pub(crate) fn new(ui: &Ui, f: impl FnMut(Event, &AnyObject) + 'static) -> Retained<Self> {
let this = Self::alloc(ui.mtm()).set_ivars(RefCell::new(Some(Box::new(f))));
unsafe { msg_send![super(this), init] }
}

fn emit(&self, event: Event, sender: &AnyObject) {
let _keep_alive = self.retain();
if let Ok(mut cb) = self.ivars().try_borrow_mut() {
if let Some(cb) = cb.as_mut() {
callback(|| cb(event, sender));
}
}
}

pub(crate) fn for_window(&self, window: &NSWindow) {
window.setDelegate(Some(ProtocolObject::from_ref(self)));
}
}
Loading
Loading