Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better class declaration #30

Closed
8 tasks done
madsmtm opened this issue Sep 5, 2021 · 5 comments
Closed
8 tasks done

Better class declaration #30

madsmtm opened this issue Sep 5, 2021 · 5 comments
Labels
A-objc2 Affects the `objc2`, `objc2-exception-helper` and/or `objc2-encode` crates enhancement New feature or request
Milestone

Comments

@madsmtm
Copy link
Owner

madsmtm commented Sep 5, 2021

Blocked on at least #21.

Fundamentally cannot be made safe, since you're calling into unknown Objective-C classes. Not even sure add_ivar is sound (since Objective-C could be using the same ivar in a superclass)?

@madsmtm madsmtm added the enhancement New feature or request label Sep 5, 2021
@madsmtm madsmtm mentioned this issue Sep 5, 2021
80 tasks
@madsmtm
Copy link
Owner Author

madsmtm commented Feb 26, 2022

Work on syntax extension that would help with declaring new classes (ideas in part from objrs, example from GNUStep's GSTitleView):

unsafe pub struct MyTitleView: NSView {
    pub close_button: Option<Id<NSButton, Owned>>,
    pub miniaturize_button: Option<Id<NSButton, Owned>>,
    pub text_attributes: Id<NSMutableDictionary, Owned>,
    pub title_color: Id<NSColor, Owned>,

    owner: *mut Object,
    owned_by_menu: Bool,
    is_key_window: Bool,
    is_main_window: Bool,
    is_active_application: Bool,
}

Approximate Objective-C equivalent.

/// Initialization & deallocation
unsafe impl MyTitleView {
    fn height() -> CGFloat {
        let h: CGFloat = unsafe { msg_send![class!(NSMenuView) menuBarHeight] };
        h + 1.0
    }

    // TODO: How do we handle `self: Option<...>`?
    fn init(self: Id<MaybeUninit<Self>, Owned>) -> Id<Self, Owned> {
        let self = unsafe { msg_send_id![super(self, class!(NSView)), init] };

        self.owner = ptr::null_mut();
        self.owned_by_menu = Bool::NO;
        self.is_key_window = Bool::NO;
        self.is_main_window = Bool::NO;
        self.is_active_application = Bool::NO;

        unsafe { msg_send![self, setAutoresizingMask: NSViewWidthSizable | NSViewMinYMargin] };

        self.text_attributes = unsafe { NSMutableDictionary::from(&[
            (msg_send_id![class!(NSFont), boldSystemFontOfSize: 0], NSFontAttributeName),
            (msg_send_id![class!(NSFont), boldSystemFontOfSize: 0], NSForegroundColorAttributeName),
        ])};

        self.title_color = msg_send_id![NSColor, lightGrayColor];

        unsafe { self.assume_init() }
    }

    #[selector(initWithOwner:)]
    fn init_with_owner(self: Id<MaybeUninit<Self>, Owned>, owner: *mut Object) -> Id<Self, Owned> {
         let self = unsafe { msg_send_id![self, init] };
         let _: () = unsafe { msg_send![self, setOwner: owner] };
         self
    }

    #[selector(setOwner:)]
    fn set_owner(&mut self, owner: Id<NSObject, Shared>) {
        let center = NSNotificationCenter::default_center();

        if (owner.is_kind_of(class!(NSWindow)) {
            log::debug("GSTitleView: owner is NSWindow or NSPanel");

            self.owner = owner.as_ptr();
            self.owned_by_menu = Bool::NO;

            msg_send![
                self,
                setFrame: NSRect::new(
                    -1,
                    self.owner.frame().size.height - MyTitleView::height() - 40,
                    self.owner.frame().size.width + 2,
                    MyTitleView::height()
                ),
            ];

            if (msg_send![self.owner, styleMask] & NSClosableWindowMask) {
                msg_send![self, addCloseButtonWithAction: sel!(performClose:)];
            }

            if (msg_send![self.owner, styleMask] & NSMiniaturizableWindowMask) {
                msg_send![self, addMiniaturizeButtonWithAction: sel!(performMiniaturize:)];
            }

            center.add_observer(self, sel!(windowBecomeKey:), NSWindowDidBecomeKeyNotification, self.owner);
            center.add_observer(self, sel!(windowResignKey:), NSWindowDidResignKeyNotification, self.owner);
            center.add_observer(self, sel!(windowBecomeMain:), NSWindowDidBecomeMainNotification, self.owner);
            center.add_observer(self, sel!(windowResignMain:), NSWindowDidResignMainNotification, self.owner);

            center.add_observer(self, sel!(applicationBecomeActive:), NSApplicationWillBecomeActiveNotification, self.owner);
            center.add_observer(self, sel!(applicationResignActive:), NSApplicationWillResignActiveNotification, self.owner);
        } else if (owner.is_kind_of(class!(NSMenu)) {
            log::debug("GSTitleView: owner is NSMenu");
            self.owner = owner.as_ptr();
            self.owned_by_menu = Bool::YES;

            let theme: Id<GSTheme, Unknown> = unsafe { msg_send_id![class!(GSTheme), theme] };

            if let Some(color) = msg_send_id![theme, colorNamed: @"GSMenuBar", state: GSThemeNormalState] {
                self.title_color = color;
            } else {
                self.title_color = msg_send_id![NSColor, blackColor];
            }

            let text_color = unsafe {
                msg_send_id![theme, colorNamed: @"GSMenuBarTitle", state: GSThemeNormalState]
            }.unwrap_or_else(|| {
                msg_send_id![NSColor, whiteColor]
            });
            [self.text_attributes, setObject: text_color, forKey: NSForegroundColorAttributeName];
        } else {
            log::debug!("GSTitleView: {} owner is not NSMenu or NSWindow or NSPanel", owner.class().name());
        }
    }

    fn owner(&self) -> *mut Object {
        self.owner
    }

    fn dealloc(&mut self) {
        if (self.owned_by_menu.is_false()) {
            unsafe { 
                let center = msg_send_id![class!(NSNotificationCenter), defaultCenter];
                msg_send![center, removeObserver: self];
            };
        }

        unsafe { msg_send![msg_send_id![class!(GSTheme), theme], setName: ptr::null(), forElement: msg_send![self.close_button, cell], temporary: Bool::NO] };

        unsafe { msg_send![super(self, class!(NSView)), dealloc] };

        // Drop impl called after this
    }
}

/// Drawing
unsafe impl MyTitleView {
    fn title_size(&self) -> NSSize {
        let s: Id<NSString, Shared> = unsafe { msg_send_id![self.owner, title] };
        s.size_with_attributes(self.text_attributes)
    }

    fn title_size(&self) -> NSSize {
        let s: Id<NSString, Shared> = unsafe { msg_send_id![self.owner, title] };
        s.size_with_attributes(self.text_attributes)
    }
}


/// Mouse actions
unsafe impl MyTitleView {
    #[selector(acceptsFirstMouse:)]
    fn accepts_first_mouse(&self, _event: &NSEvent) -> Bool {
        Bool::YES
    }

    #[selector(mouseDown:)]
    fn mouse_down(&self, _event: &NSEvent) {
        todo!()
    }

    #[selector(rightMouseDown:)]
    fn right_mouse_down(&self, _event: &NSEvent) {
        // Explicitly does not call super
    }

    #[selector(menuForEvent:)]
    fn menu_for_event(&self, _event: &NSEvent) -> Option<Id<NSMenu, Unknown>> {
        None
    }
}


/// NSWindow & NSApplication notifications
unsafe impl MyTitleView {
    #[selector(applicationBecomeActive:)]
    fn application_becomes_active(&self, _notification: &NSNotification) {
        self.is_active_application = Bool::YES;
    }

    // ...
}

// ...

Approximate Objective-C equivalent.

@madsmtm
Copy link
Owner Author

madsmtm commented Jun 7, 2022

Regarding instance variables, an idea would be to have a macro that expands to roughly:

struct MyObjectIvars {
    ivar1: i32,
    ivar2: Box<u8>,
}

impl MyObject {
    fn ivars(&self) -> &MyObjectIvars { ... }
    fn ivars_mut(&mut self) -> &mut MyObjectIvars { ... }
}

(Assuming that the order that ivars are laid out in are guaranteed, haven't researched this yet).

That would then make it trivial for users to access these as self.ivars().ivar1, and we avoid having to write a complex macro that translates self.ivar1 to extract_ivar1(self) like objrs does.

EDIT: Found a better solution to this, see #190

This was referenced Jun 15, 2022
@madsmtm
Copy link
Owner Author

madsmtm commented Jun 16, 2022

Idea for init: Add PartialInit<T>(NonNull<T>) struct with helper methods for retrieving &mut MaybeUninit<U> references to T's instance variables:

fn init(this: Allocated<Self>) -> Id<Self, Owned> {
    let this: Option<PartialInit<Self>> = unsafe { msg_send_id![super(self, class!(NSView)), init] };
    this.map(|mut this| {
        // this.owner is MaybeUninit<*mut Object>
        this.owner.write(ptr::null_mut());
        // this.owned_by_menu is MaybeUninit<Bool>
        this.owned_by_menu.write(Bool::NO);
        this.is_key_window.write(Bool::NO);
        this.is_main_window.write(Bool::NO);
        this.is_active_application.write(Bool::NO);
    
        // Discouraged; no way to verify that `this` is initialized!
        unsafe { msg_send![this, setAutoresizingMask: NSViewWidthSizable | NSViewMinYMargin] };
    
        this.text_attributes.write(unsafe { NSMutableDictionary::from(&[
            (msg_send_id![class!(NSFont), boldSystemFontOfSize: 0], NSFontAttributeName),
            (msg_send_id![class!(NSFont), boldSystemFontOfSize: 0], NSForegroundColorAttributeName),
        ])});
    
        this.title_color.write(unsafe { msg_send_id![NSColor, lightGrayColor] });
    
        unsafe { this.assume_init() }
    })
}

EDIT: Postponed, see #252 for the interim solution

@madsmtm
Copy link
Owner Author

madsmtm commented Jun 19, 2022

We want to keep the "declare this class" and "use this class" use-cases separate (may at some point merge these together again, but at least for now). The "use this class" pattern will probably be implemented as part of #161.

Reasoning: Declared classes are often used for delegate classes, who don't need to be called from Rust, and it would be a waste trying to create methods for doing so. More importantly, if you do self.my_method() inside a declared class method, it is ambiguous whether you want that to be a plain Rust-to-Rust method call, or you want it to go through the msg_send! machinery (Rust-to-Objective-C-to-Rust).

@madsmtm
Copy link
Owner Author

madsmtm commented Nov 3, 2022

The situation has gone a long way since I opened this issue!

A few remaining things are #282 and #283, but I went and opened separate issues for that to make it easier to track progress.

See also #250 (comment) about improving safety when implementing protocols.

@madsmtm madsmtm closed this as completed Nov 3, 2022
@madsmtm madsmtm added the A-objc2 Affects the `objc2`, `objc2-exception-helper` and/or `objc2-encode` crates label Nov 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-objc2 Affects the `objc2`, `objc2-exception-helper` and/or `objc2-encode` crates enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant