fltk_observe/
lib.rs

1#![doc = include_str!("../README.md")]
2#![allow(clippy::needless_doctest_main)]
3
4use fltk::{
5    app,
6    enums::{Event, Shortcut},
7    menu::MenuFlag,
8    prelude::*,
9    window::Window,
10};
11use std::{
12    any::Any,
13    sync::{Mutex, OnceLock},
14};
15
16static STATE: OnceLock<Mutex<Box<dyn Any + Send + Sync>>> = OnceLock::new();
17
18macro_rules! state_ref {
19    () => {
20        STATE
21            .get()
22            .expect("Global state not initialized.")
23            .lock()
24            .expect("Failed to lock global state.")
25            .downcast_ref()
26            .expect("State type mismatch (did you init a different type?)")
27    };
28}
29
30macro_rules! state_mut {
31    () => {
32        STATE
33            .get()
34            .expect("Global state not initialized.")
35            .lock()
36            .expect("Failed to lock global state.")
37            .downcast_mut()
38            .expect("State type mismatch (did you init a different type?)")
39    };
40}
41
42/// The event used to trigger UI updates when state changes.
43pub const STATE_CHANGED: Event = Event::from_i32(100);
44
45/// A trait to bind actions and views to widgets in response to shared state.
46pub trait WidgetObserver<T, W> {
47    /// Sets the action to be executed when the widget is interacted with.
48    /// The function receives mutable access to the shared state and a reference to the widget.
49    fn set_action<Listen: Clone + 'static + Fn(&mut T, &Self)>(&mut self, l: Listen);
50    /// Binds a view function that updates the widget whenever the state changes.
51    /// This is automatically triggered on `STATE_CHANGED`.
52    fn set_view<Update: Clone + 'static + Fn(&T, &mut Self)>(&mut self, u: Update);
53}
54
55/// A trait to bind state-aware actions to FLTK menu items.
56pub trait MenuObserver<T, W> {
57    /// Adds a menu item and attaches a state-aware action to it.
58    fn add_action<Listen: Clone + 'static + Fn(&mut T, &Self)>(
59        &mut self,
60        label: &str,
61        shortcut: Shortcut,
62        flags: MenuFlag,
63        l: Listen,
64    );
65}
66
67impl<T: Send + Sync + 'static, W: WidgetExt + WidgetBase + 'static + Clone> WidgetObserver<T, W>
68    for W
69{
70    fn set_action<Listen: Clone + 'static + Fn(&mut T, &Self)>(&mut self, l: Listen) {
71        self.set_callback(move |w| {
72            let win = unsafe { Window::from_widget_ptr(w.window().unwrap().as_widget_ptr()) };
73            l(state_mut!(), w);
74            app::handle(STATE_CHANGED, &win.clone()).ok();
75        });
76    }
77
78    fn set_view<Update: Clone + 'static + Fn(&T, &mut Self)>(&mut self, u: Update) {
79        let w = self.clone();
80        let func = move || {
81            let mut w = w.clone();
82            u(state_ref!(), &mut w);
83        };
84        func();
85        self.handle(move |_w, ev| {
86            if ev == STATE_CHANGED {
87                func();
88            }
89            false
90        });
91    }
92}
93
94impl<T: Send + Sync + 'static, W: MenuExt + 'static + Clone> MenuObserver<T, W> for W {
95    fn add_action<Listen: Clone + 'static + Fn(&mut T, &Self)>(
96        &mut self,
97        label: &str,
98        shortcut: Shortcut,
99        flags: MenuFlag,
100        l: Listen,
101    ) {
102        self.add(label, shortcut, flags, move |w| {
103            let win = unsafe { Window::from_widget_ptr(w.window().unwrap().as_widget_ptr()) };
104            l(state_mut!(), w);
105            app::handle(STATE_CHANGED, &win).ok();
106        });
107    }
108}
109
110/// Provides the ability to initialize global application state for observation.
111pub trait Runner<State: 'static + Send + Sync> {
112    /// Initializes the global state using the given closure.
113    /// Should be called early in `main()` before accessing state.
114    fn use_state<F: 'static + FnOnce() -> State>(self, init: F) -> Option<Self>
115    where
116        Self: Sized;
117}
118
119impl<State: 'static + Send + Sync> Runner<State> for app::App {
120    fn use_state<F: 'static + FnOnce() -> State>(self, init: F) -> Option<Self>
121    where
122        Self: Sized,
123    {
124        STATE.set(Mutex::new(Box::new((init)()))).ok()?;
125        Some(self)
126    }
127}
128
129/// Mutably accesses the global state and applies the given closure.
130/// Triggers a UI update by sending `STATE_CHANGED` to the main window.
131pub fn with_state_mut<State: 'static, F: FnOnce(&mut State) + Clone>(f: F) {
132    f(state_mut!());
133    app::handle_main(STATE_CHANGED).ok();
134}
135
136/// Mutably accesses the global state and applies the given closure.
137/// Also emits `STATE_CHANGED` to the given window.
138pub fn with_state_mut_on<State: 'static, F: FnOnce(&mut State) + Clone>(
139    win: &impl WindowExt,
140    f: F,
141) {
142    f(state_mut!());
143    app::handle(STATE_CHANGED, win).ok();
144}
145
146/// Provides read-only access to the global state for the given closure.
147pub fn with_state<State: 'static, F: FnOnce(&State) + Clone>(f: F) {
148    f(state_ref!());
149}
150
151/// Triggers a global UI update by emitting `STATE_CHANGED` on the main window and waking the app.
152pub fn notify() {
153    app::handle_main(STATE_CHANGED).ok();
154    app::awake();
155}
156
157/// Triggers a UI update for the specified window.
158/// Also calls `app::awake()` to wake up the event loop.
159pub fn notify_win(win: &impl WindowExt) {
160    app::handle(STATE_CHANGED, win).ok();
161    app::awake();
162}