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

Cannot get NSDistributedNotificationCenter to work with the addObserverForName:object:queue:usingBlock: message #644

Closed
yavko opened this issue Aug 10, 2024 · 4 comments
Labels
A-framework Affects the framework crates and the translator for them

Comments

@yavko
Copy link

yavko commented Aug 10, 2024

I'm working on a library that should allow for the fetching/listening of the OS's accent color setting similar to dark-light but for accent colors, I've got every OS working except macOS. For listening to the accent color changes, I've tried changing it in all sorts of ways, and it still will not notify on OS changes. Any help would be appreciated.

Below here is the code I've written as a prototype to get it working, I'm creating a NSDistributedNotificationCenter using the defaultCenter message, I'm creating a block that transmits onto an unbounded channel, which is listened on for the accent color changes, the block is then wrapped into a RcBlock and then I send the addObserverForName:object:queue:usingBlock: message with the block and the notification name AppleColorPreferencesChangedNotification. I'm not sure if my code calling objc2 is wrong, or not, but I also feel like there isn't a way it could be my non objc2 code, because the block, would be at least printing the debug message which is defined in the block. My code also uses an intermediary stream, so I can implement Drop on the stream, because I'm using the async-stream crate, however, I'm not sure if this is required, so I'm also unsure about that. Also, sorry for the messy code.

use thiserror::Error;
use tokio_stream::{Stream, StreamExt};

#[derive(Eq, PartialEq, PartialOrd, Ord)]
pub struct Color(pub u8, pub u8, pub u8);

#[derive(Error, Debug)]
pub enum AccentError {
    #[cfg(target_os = "macos")]
    #[error("an error occured while awaiting getting the accent color")]
    JoinError(#[from] tokio::task::JoinError),
    #[error("An error occured: {0}")]
    StringError(String),
}

pub type AccentResult<T> = Result<T, AccentError>;

#[cfg(target_os = "macos")]
pub async fn get_accent_color() -> AccentResult<Color> {
    use objc2_app_kit::NSColor;
    tokio::task::spawn_blocking(|| {
        let color = unsafe { NSColor::controlAccentColor() };
        Ok(macos_stuff::convert_color(color))
    })
    .await?
}


#[cfg(target_os = "macos")]
mod macos_stuff {
    use super::*;
    use objc2::rc::Retained;
    use objc2_app_kit::{NSColor, NSColorSpace};
    use objc2_foundation::NSDistributedNotificationCenter;
    use tokio::sync::mpsc::UnboundedReceiver;

    pub(crate) fn convert_color(color: Retained<NSColor>) -> Color {
        let color = unsafe { color.colorUsingColorSpace(&*NSColorSpace::sRGBColorSpace()) }
            .expect("failed to convert colorspace");
        let (mut r, mut g, mut b, mut a) = (0.0, 0.0, 0.0, 0.0);
        unsafe { color.getRed_green_blue_alpha(&mut r, &mut g, &mut b, &mut a) };
        Color(
            (255.0 * r).round() as u8,
            (255.0 * g).round() as u8,
            (255.0 * b).round() as u8,
        )
    }
    pub(crate) struct AccentStream {
        pub observer: Retained<objc2_foundation::NSObject>,
        pub receiver: UnboundedReceiver<()>,
    }
    impl Stream for AccentStream {
        type Item = ();
        fn poll_next(
            mut self: std::pin::Pin<&mut Self>,
            cx: &mut std::task::Context<'_>,
        ) -> std::task::Poll<Option<Self::Item>> {
            println!("[polling stream]");
            let res = self.receiver.poll_recv(cx);
            println!("[polled stream: {res:#?}]");
            res
        }
    }

    impl Drop for AccentStream {
        fn drop(&mut self) {
            println!("[dropping the stream]");
            let ns_center = unsafe { NSDistributedNotificationCenter::defaultCenter() };
            let Self { observer, receiver } = self;
            receiver.close();
            unsafe { ns_center.removeObserver(&*observer) };
        }
    }
}

#[cfg(target_os = "macos")]
pub async fn accent_color_stream() -> AccentResult<impl Stream<Item = Color>> {
    use async_stream::stream;
    use block2::RcBlock;
    use macos_stuff::AccentStream;
    use objc2_foundation::{ns_string, NSDistributedNotificationCenter};
    use std::ptr::NonNull;
    use tokio::sync::mpsc::unbounded_channel;
    println!("[creating channel]");
    let (tx, rx) = unbounded_channel::<()>();

    println!("[creating notification center]");
    let notification_name = ns_string!("AppleColorPreferencesChangedNotification");
    let ns_center = unsafe { NSDistributedNotificationCenter::defaultCenter() };
    println!("[creating block]");
    use objc2_foundation::NSNotification;
    let block = move |_: NonNull<NSNotification>| {
        println!("[sending message]");
        tx.send(()).unwrap();
        println!("[sent message]");
    };
    let block = RcBlock::new(block);

    println!("[creating observer]");
    let observer = unsafe {
        ns_center.addObserverForName_object_queue_usingBlock(
            Some(notification_name),
            None,
            None,
            &*block,
        )
    };
    println!("[creating stream wrapper w/ dropping]");
    let mut stream = AccentStream {
        observer,
        receiver: rx,
    };
    Ok(stream! {
        println!("[entering stream loop]");
        while let Some(_) = stream.next().await {
            println!("[getting color]");
            yield get_accent_color().await.expect("how did u get here...");
        }
        println!("[exiting stream loop]");
    })
}

Additionally, for context I referenced some of the code from https://github.com/freethinkel/tauri-plugin-accent-color/blob/main/src/lib.rs, at first I tried writing this library with objc2, gave up, tried with objc, but still could not get it working, and then switched back to objc2 however since the repo referenced is not written using objc2, I had to figure out different ways of doing things for actually converting the accent color.

@madsmtm madsmtm added the A-framework Affects the framework crates and the translator for them label Aug 11, 2024
@madsmtm
Copy link
Owner

madsmtm commented Aug 11, 2024

The documentation for NSDistributedNotificationCenter says:

Distributed notifications are delivered via a task’s run loop. A task must be running a run loop in one of the “common” modes, such as NSDefaultRunLoopMode, to receive a distributed notification.

That is, blocking the main thread using tokio and such is not sufficient for getting this event to trigger - you have to be actively polling the system using NSRunLoop::run, NSApplicationMain or likewise.

In general, the async story in Rust for GUI applications / functionality on macOS/iOS is not really fleshed out, if you can avoid it in this case, I'd suggest you do so (see also #279).

@yavko
Copy link
Author

yavko commented Aug 11, 2024

The documentation for NSDistributedNotificationCenter says:

Distributed notifications are delivered via a task’s run loop. A task must be running a run loop in one of the “common” modes, such as NSDefaultRunLoopMode, to receive a distributed notification.

That is, blocking the main thread using tokio and such is not sufficient for getting this event to trigger - you have to be actively polling the system using NSRunLoop::run, NSApplicationMain or likewise.

In general, the async story in Rust for GUI applications / functionality on macOS/iOS is not really fleshed out, if you can avoid it in this case, I'd suggest you do so (see also #279).

Would it be better to just have a busy loop that polls the accent color like so, or would it be more efficient to figure out polling with NSRunLoop

let mut last = get_accent_color().await?.into_color();
let stream = stream! {
	while let Ok(current) = get_accent_color().await {
		let current_conv = current.into_color();
		if last != current_conv {
			last = current_conv;
			yield current_conv;
		}
	}
};

@madsmtm
Copy link
Owner

madsmtm commented Aug 11, 2024

would it be more efficient to figure out polling with NSRunLoop

Much more, as the OS can put the process to sleep (which a busy loop will not). But it depends on your use-case, maybe it'll be fine for you to check e.g. every 60 seconds whether the color changed.

@madsmtm
Copy link
Owner

madsmtm commented Sep 17, 2024

I'm going to close this issue, since it isn't really actionable, this is a fundamental limitation of Apple's frameworks that you have to know about to use them effectively. The best we can do, until #279 is resolved at an ecosystem level, is add more documentation, which I have done here as part of #650. Feel free to say if there's something in that documentation that's unclear, then I'll try to reword it!

@madsmtm madsmtm closed this as completed Sep 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-framework Affects the framework crates and the translator for them
Projects
None yet
Development

No branches or pull requests

2 participants