use std::sync::{Arc, Mutex};
#[must_use = "State must be used to track or display values"]
pub struct State<T: Send + 'static> {
inner: Arc<Mutex<StateInner<T>>>,
}
type StateListener<T> = Box<dyn Fn(&T) + Send>;
struct StateInner<T> {
value: T,
listeners: Vec<StateListener<T>>,
}
impl<T: Send + 'static> State<T> {
pub fn new(initial: T) -> Self {
Self {
inner: Arc::new(Mutex::new(StateInner {
value: initial,
listeners: Vec::new(),
})),
}
}
pub fn get(&self) -> T
where
T: Clone,
{
self.inner.lock().unwrap().value.clone()
}
pub fn set(&self, value: T) {
let mut inner = self.inner.lock().unwrap();
inner.value = value;
for listener in &inner.listeners {
listener(&inner.value);
}
}
pub fn update(&self, f: impl FnOnce(&mut T))
where
T: Clone,
{
let mut inner = self.inner.lock().unwrap();
f(&mut inner.value);
for listener in &inner.listeners {
listener(&inner.value);
}
}
pub fn on_change(&self, f: impl Fn(&T) + Send + 'static) {
self.inner.lock().unwrap().listeners.push(Box::new(f));
}
pub fn modify(&self, f: impl FnOnce(&mut T))
where
T: Clone,
{
self.update(f);
}
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> U
where
T: Clone,
{
let inner = self.inner.lock().unwrap();
f(&inner.value)
}
pub fn bind<F>(&self, format_fn: F) -> Binding
where
T: Clone + std::str::FromStr,
F: Fn(&T) -> String + Send + 'static,
{
let binding = Binding {
update_callback_id: Arc::new(Mutex::new(None)),
};
let state = self.clone();
let update_id = crate::event::register_value_with_auto_id(move |s: String| {
if let Ok(val) = s.parse::<T>() {
state.set(val);
}
});
*binding.update_callback_id.lock().unwrap() = Some(update_id);
self.on_change(move |val| {
let text = format_fn(val);
crate::event::dispatch_value(update_id, text.clone());
});
binding
}
}
impl<T: Send + 'static> Clone for State<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
pub struct Binding {
update_callback_id: Arc<Mutex<Option<usize>>>,
}
impl Binding {
pub fn callback_id(&self) -> Option<usize> {
*self.update_callback_id.lock().unwrap()
}
}
pub fn counter_state(increment_id: usize, decrement_id: usize, reset_id: usize) -> State<i64> {
let state = State::new(0_i64);
{
let s = state.clone();
crate::event::register(increment_id, move || {
s.update(|v| *v += 1);
});
}
{
let s = state.clone();
crate::event::register(decrement_id, move || {
s.update(|v| *v -= 1);
});
}
{
let s = state.clone();
crate::event::register(reset_id, move || {
s.set(0);
});
}
state
}
pub fn state<T: Send + 'static>(initial: T) -> State<T> {
State::new(initial)
}
impl State<i64> {
pub fn increment(&self) {
self.update(|v| *v += 1);
}
pub fn decrement(&self) {
self.update(|v| *v -= 1);
}
pub fn reset(&self) {
self.set(0);
}
}
impl State<String> {
pub fn append(&self, s: &str) {
self.update(|v| v.push_str(s));
}
pub fn clear(&self) {
self.set(String::new());
}
}
impl State<bool> {
pub fn toggle(&self) {
self.update(|v| *v = !*v);
}
}
#[cfg(not(test))]
pub fn bind_label<T: Send + std::fmt::Display + 'static>(
state: &State<T>,
view_tag: isize,
format_fn: impl Fn(&T) -> String + Send + 'static,
) {
state.on_change(move |val| {
let text = format_fn(val);
update_label_by_tag(view_tag, &text);
});
}
#[cfg(not(test))]
pub fn update_label_by_tag(tag: isize, text: &str) {
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
unsafe {
let ns_app: *mut Object =
msg_send![Class::get("NSApplication").unwrap(), sharedApplication];
let window: *mut Object = msg_send![ns_app, keyWindow];
if window.is_null() {
return;
}
let content: *mut Object = msg_send![window, contentView];
if content.is_null() {
return;
}
let target: *mut Object = msg_send![content, viewWithTag: tag];
if !target.is_null() {
let cstr = std::ffi::CString::new(text).unwrap();
let ns: *mut Object =
msg_send![objc::class!(NSString), stringWithUTF8String: cstr.as_ptr()];
let _: () = msg_send![target, setStringValue: ns];
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicI64, Ordering};
#[test]
fn test_state_get_set() {
let s = State::new(42_i64);
assert_eq!(s.get(), 42);
s.set(100);
assert_eq!(s.get(), 100);
}
#[test]
fn test_state_update() {
let s = State::new(10_i64);
s.update(|v| *v += 5);
assert_eq!(s.get(), 15);
}
#[test]
fn test_state_on_change() {
let s = State::new(0_i64);
let observed = Arc::new(AtomicI64::new(0));
let o = observed.clone();
s.on_change(move |v| {
o.store(*v, Ordering::SeqCst);
});
s.set(42);
assert_eq!(observed.load(Ordering::SeqCst), 42);
s.update(|v| *v += 8);
assert_eq!(observed.load(Ordering::SeqCst), 50);
}
#[test]
fn test_state_clone_shares() {
let s1 = State::new(0_i64);
let s2 = s1.clone();
s1.set(99);
assert_eq!(s2.get(), 99);
}
#[test]
fn test_counter_state() {
let s = counter_state(901, 902, 903);
crate::event::dispatch(901);
assert_eq!(s.get(), 1);
crate::event::dispatch(901);
assert_eq!(s.get(), 2);
crate::event::dispatch(902);
assert_eq!(s.get(), 1);
crate::event::dispatch(903);
assert_eq!(s.get(), 0);
}
#[test]
fn test_state_modify_alias() {
let s = State::new(3_i64);
s.modify(|v| *v *= 2);
assert_eq!(s.get(), 6);
}
#[test]
fn test_state_map() {
let s = State::new(11_i64);
let doubled = s.map(|v| *v * 2);
assert_eq!(doubled, 22);
}
#[test]
fn test_state_string_helpers() {
let s = State::new(String::from("a"));
s.append("bc");
assert_eq!(s.get(), "abc");
s.clear();
assert_eq!(s.get(), "");
}
#[test]
fn test_state_bool_toggle() {
let s = State::new(false);
s.toggle();
assert!(s.get());
s.toggle();
assert!(!s.get());
}
#[test]
fn test_binding_callback_id() {
let s = State::new(0_i32);
let b = s.bind(|v| v.to_string());
assert!(b.callback_id().is_some());
}
}