Files
ghostty-rs/crates/ghostty-macos/src/helpers/published.rs
T
ddidderr 4861015105 feat(macos): foundation of the macOS app ported from Swift to Rust
Phase 9, MA1: crates/ghostty-macos (AppKit via objc2; empty stub on
non-Apple targets) ports the foundation of Ghostty 1.3.1's Swift macOS
app (ghostty/macos): main, AppDelegate (+Ghostty) with lifecycle, dock
menu, open-file, notifications, shortcut sync and quit/reopen paths;
MainMenu.xib as generated menu tables built into NSMenus; the
Ghostty.App/Config/Surface wrappers driving libghostty in-process via
ghostty-lib; the generic helpers; scripts/macos-bundle.sh to assemble
Ghostty.app (dry-run on Linux). Feature areas are stubbed for MA2-MA5.
Design and type mapping: docs/rewrite/notes/macos-app-design.md
(App Intents is a Swift-only framework and cannot be ported).

Compile-checked only (no macOS here): just check-macos passes for
aarch64/x86_64-apple-darwin with -D warnings; 125 platform-independent
tests run on Linux.

Test Plan:
- just check-macos
- cargo test -p ghostty-macos
2026-10-12 19:25:44 +02:00

146 lines
3.9 KiB
Rust

//! The Rust equivalent of a SwiftUI `@Published` property / Combine
//! `CurrentValueSubject`: a value cell that remembers its latest value and
//! notifies subscribers after each change with the new value (Combine's
//! `$property` publisher emits in `willSet`, but the Swift call sites only use
//! the value they are handed, which is the new one).
//!
//! Single-threaded (main thread), like everything that owns AppKit objects.
//! Dropping the returned [`Subscription`] cancels it (an `AnyCancellable`).
use std::{cell::RefCell, rc::Rc};
type Callback<T> = Rc<dyn Fn(&T)>;
struct Inner<T> {
value: T,
next_id: u64,
subscribers: Vec<(u64, Callback<T>)>,
}
/// `@Published var value: T`. Cloning shares the same cell.
pub struct Published<T> {
inner: Rc<RefCell<Inner<T>>>,
}
impl<T> Clone for Published<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
/// Cancels the subscription when dropped.
#[must_use = "dropping the subscription cancels it"]
pub struct Subscription {
cancel: Option<Box<dyn FnOnce()>>,
}
impl Subscription {
/// Keep the subscription for the life of the cell.
pub fn forget(mut self) {
self.cancel = None;
}
}
impl Drop for Subscription {
fn drop(&mut self) {
if let Some(cancel) = self.cancel.take() {
cancel();
}
}
}
impl<T: Clone + 'static> Published<T> {
pub fn new(value: T) -> Self {
Self {
inner: Rc::new(RefCell::new(Inner {
value,
next_id: 0,
subscribers: Vec::new(),
})),
}
}
/// The current value.
#[must_use]
pub fn get(&self) -> T {
self.inner.borrow().value.clone()
}
/// Assign a new value and notify every subscriber with it.
pub fn set(&self, value: T) {
let subscribers: Vec<Callback<T>> = {
let mut inner = self.inner.borrow_mut();
inner.value = value.clone();
inner.subscribers.iter().map(|(_, cb)| cb.clone()).collect()
};
for cb in subscribers {
cb(&value);
}
}
/// Mutate in place (`value.field = x` on a `@Published` struct) and notify.
pub fn update(&self, f: impl FnOnce(&mut T)) {
let mut value = self.get();
f(&mut value);
self.set(value);
}
/// Subscribe (`$property.sink`). Like Combine's `@Published` publisher the
/// callback is first invoked with the current value.
pub fn sink(&self, f: impl Fn(&T) + 'static) -> Subscription {
let cb: Callback<T> = Rc::new(f);
let (id, current) = {
let mut inner = self.inner.borrow_mut();
let id = inner.next_id;
inner.next_id += 1;
inner.subscribers.push((id, cb.clone()));
(id, inner.value.clone())
};
cb(&current);
let weak = Rc::downgrade(&self.inner);
Subscription {
cancel: Some(Box::new(move || {
if let Some(inner) = weak.upgrade() {
inner.borrow_mut().subscribers.retain(|(i, _)| *i != id);
}
})),
}
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use super::*;
#[test]
fn sink_gets_current_then_changes() {
let p = Published::new(1);
let seen = Rc::new(RefCell::new(Vec::new()));
let _s = p.sink({
let seen = seen.clone();
move |v| seen.borrow_mut().push(*v)
});
p.set(2);
p.update(|v| *v += 5);
assert_eq!(*seen.borrow(), vec![1, 2, 7]);
assert_eq!(p.get(), 7);
}
#[test]
fn cancel_on_drop() {
let p = Published::new(0);
let n = Rc::new(Cell::new(0));
let s = p.sink({
let n = n.clone();
move |_| n.set(n.get() + 1)
});
drop(s);
p.set(1);
assert_eq!(n.get(), 1);
}
}