Skip to main content

floem/
action.rs

1#![deny(missing_docs)]
2
3//! Action functions that can be called anywhere in a Floem application
4//!
5//! This module includes a variety of functions that can interact with the window from which the function is being called.
6//!
7//! This includes, moving the window, resizing the window, adding context menus and overlays, and running a callback after a specified duration.
8
9use std::sync::atomic::AtomicU64;
10
11use floem_reactive::{SignalWith, UpdaterEffect};
12use peniko::kurbo::{Point, Size, Vec2};
13use winit::window::WindowId;
14use winit::window::{ResizeDirection, Theme};
15
16use crate::IntoView;
17use crate::platform::{Duration, Instant};
18
19use crate::{
20    app::{AppUpdateEvent, add_app_update_event},
21    message::{UPDATE_MESSAGES, UpdateMessage},
22    platform::menu::Menu,
23    view::View,
24    view::ViewId,
25    views::Decorators,
26    window::handle::{get_current_view, set_current_view},
27    window::tracking::with_window,
28};
29
30#[cfg(not(target_arch = "wasm32"))]
31pub use crate::platform::file_action::*;
32
33/// Add an update message
34pub(crate) fn add_update_message(msg: UpdateMessage) {
35    let current_view = get_current_view();
36    let _ = UPDATE_MESSAGES.try_with(|msgs| {
37        let mut msgs = msgs.borrow_mut();
38        msgs.entry(current_view).or_default().push(msg);
39    });
40}
41
42/// Toggle whether the window is maximized or not.
43pub fn toggle_window_maximized() {
44    add_update_message(UpdateMessage::ToggleWindowMaximized);
45}
46
47/// Set the maximized state of the window.
48pub fn set_window_maximized(maximized: bool) {
49    add_update_message(UpdateMessage::SetWindowMaximized(maximized));
50}
51
52/// Minimize the window.
53pub fn minimize_window() {
54    add_update_message(UpdateMessage::MinimizeWindow);
55}
56
57/// If and while the mouse is pressed, allow the window to be dragged.
58pub fn drag_window() {
59    add_update_message(UpdateMessage::DragWindow);
60}
61
62/// If and while the mouse is pressed, allow the window to be resized.
63pub fn drag_resize_window(direction: ResizeDirection) {
64    add_update_message(UpdateMessage::DragResizeWindow(direction));
65}
66
67/// Move the window by a specified delta.
68pub fn set_window_delta(delta: Vec2) {
69    add_update_message(UpdateMessage::SetWindowDelta(delta));
70}
71
72/// Set the window scale.
73///
74/// This will scale all view elements in the renderer.
75pub fn set_window_scale(window_scale: f64) {
76    add_update_message(UpdateMessage::WindowScale(window_scale));
77}
78
79/// Send a message to the application to open the Inspector for this Window.
80pub fn inspect() {
81    add_update_message(UpdateMessage::Inspect);
82}
83
84/// Set the **global** app theme in all windows.
85///
86/// Toggles both floem and window themes.
87pub fn set_global_theme(theme: Theme) {
88    add_app_update_event(AppUpdateEvent::ThemeChanged { theme });
89}
90
91/// Set the **window** theme.
92///
93/// Specify `None` to reset the theme to the system default.
94pub fn set_theme(theme: Option<Theme>) {
95    add_update_message(UpdateMessage::SetTheme(theme));
96}
97
98/// Toggle **global** app theme.
99pub fn toggle_global_theme() {
100    let theme = current_theme().unwrap_or(Theme::Dark);
101    let theme = match theme {
102        Theme::Light => Theme::Dark,
103        Theme::Dark => Theme::Light,
104    };
105    add_app_update_event(AppUpdateEvent::ThemeChanged { theme });
106}
107
108/// Toggle **window** theme.
109pub fn toggle_window_theme() {
110    let theme = current_theme().unwrap_or(Theme::Dark);
111    let theme = match theme {
112        Theme::Light => Theme::Dark,
113        Theme::Dark => Theme::Light,
114    };
115    // add_app_update_event(AppUpdateEvent::ThemeChanged { theme });
116    add_update_message(UpdateMessage::SetTheme(Some(theme)));
117}
118
119/// Get current window theme.
120pub fn current_theme() -> Option<Theme> {
121    let win_id = get_current_view().window_id()?;
122    with_window(&win_id, |w| w.theme())?
123}
124
125pub(crate) struct Timer {
126    pub(crate) token: TimerToken,
127    pub(crate) action: Box<dyn FnOnce(TimerToken)>,
128    pub(crate) deadline: Instant,
129    pub(crate) is_animation: bool,
130    pub(crate) window_id: Option<WindowId>,
131}
132
133/// A token associated with a timer.
134// TODO: what is this for?
135#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash)]
136pub struct TimerToken(u64);
137
138impl TimerToken {
139    /// A token that does not correspond to any timer.
140    pub const INVALID: TimerToken = TimerToken(0);
141
142    /// Create a new token.
143    pub fn next() -> TimerToken {
144        static TIMER_COUNTER: AtomicU64 = AtomicU64::new(0);
145        TimerToken(TIMER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed))
146    }
147
148    /// Create a new token from a raw value.
149    pub const fn from_raw(id: u64) -> TimerToken {
150        TimerToken(id)
151    }
152
153    /// Get the raw value for a token.
154    pub const fn into_raw(self) -> u64 {
155        self.0
156    }
157
158    /// Cancel a timer.
159    pub fn cancel(self) {
160        add_app_update_event(AppUpdateEvent::CancelTimer { timer: self });
161    }
162}
163
164/// Execute a callback after a specified duration.
165pub fn exec_after(duration: Duration, action: impl FnOnce(TimerToken) + 'static) -> TimerToken {
166    let view = get_current_view();
167    let action = move |token| {
168        let current_view = get_current_view();
169        set_current_view(view.root());
170        action(token);
171        set_current_view(current_view);
172    };
173
174    let token = TimerToken::next();
175    let deadline = Instant::now() + duration;
176    add_app_update_event(AppUpdateEvent::RequestTimer {
177        timer: Timer {
178            token,
179            action: Box::new(action),
180            deadline,
181            is_animation: false,
182            window_id: None,
183        },
184    });
185    token
186}
187
188/// Execute a callback on the next animation frame, synchronized with the display's refresh rate.
189///
190/// Returns a [`TimerToken`] that can be used to cancel the callback before it fires.
191/// Returns [`TimerToken::INVALID`] if called outside of a window context.
192pub fn exec_after_animation_frame(action: impl FnOnce(TimerToken) + 'static) -> TimerToken {
193    let view = get_current_view();
194    let Some(window_id) = view.window_id() else {
195        return TimerToken::INVALID;
196    };
197
198    let action = move |token| {
199        let current_view = get_current_view();
200        set_current_view(view.root());
201        action(token);
202        set_current_view(current_view);
203    };
204
205    let token = TimerToken::next();
206    add_app_update_event(AppUpdateEvent::RequestAnimationTimer {
207        timer: Timer {
208            token,
209            action: Box::new(action),
210            deadline: Instant::now(), // overridden by handler using monitor refresh rate
211            is_animation: true,
212            window_id: Some(window_id),
213        },
214        window_id,
215    });
216    token
217}
218
219/// Debounce an action.
220///
221/// This tracks a signal and checks if the inner value has changed by checking it's hash and will
222/// run the action only once an **uninterrupted** duration has passed.
223pub fn debounce_action<T, F>(signal: impl SignalWith<T> + 'static, duration: Duration, action: F)
224where
225    T: std::hash::Hash + 'static,
226    F: Fn() + Clone + 'static,
227{
228    UpdaterEffect::new_stateful(
229        move |prev_opt: Option<(u64, Option<TimerToken>)>| {
230            use std::hash::Hasher;
231            let mut hasher = std::hash::DefaultHasher::new();
232            signal.with(|v| v.hash(&mut hasher));
233            let hash = hasher.finish();
234            let execute = prev_opt
235                .map(|(prev_hash, _)| prev_hash != hash)
236                .unwrap_or(true);
237            (execute, (hash, prev_opt.and_then(|(_, timer)| timer)))
238        },
239        move |execute, (hash, prev_timer): (u64, Option<TimerToken>)| {
240            // Cancel the previous timer if it exists
241            if let Some(timer) = prev_timer {
242                timer.cancel();
243            }
244            let timer_token = if execute {
245                let action = action.clone();
246                Some(exec_after(duration, move |_| {
247                    action();
248                }))
249            } else {
250                None
251            };
252            (hash, timer_token)
253        },
254    );
255}
256
257/// Show a system context menu at the specified position.
258///
259/// Platform support:
260/// - Windows: Yes
261/// - macOS: Yes
262/// - Linux: Uses a custom Floem View
263pub fn show_context_menu(menu: Menu, pos: Option<Point>) {
264    add_update_message(UpdateMessage::ShowContextMenu { menu, pos });
265}
266
267/// Set the system window menu.
268///
269/// Platform support:
270/// - Windows: Yes
271/// - macOS: Yes
272/// - Linux: No
273/// - wasm32: No
274#[cfg(not(target_arch = "wasm32"))]
275pub fn set_window_menu(menu: Menu) {
276    add_update_message(UpdateMessage::WindowMenu { menu });
277}
278
279/// Set the title of the window.
280pub fn set_window_title(title: String) {
281    add_update_message(UpdateMessage::SetWindowTitle { title });
282}
283
284/// Clear the focus from this window
285pub fn clear_focus() {
286    add_update_message(UpdateMessage::ClearFocus);
287}
288
289/// Focus the window.
290pub fn focus_window() {
291    add_update_message(UpdateMessage::FocusWindow);
292}
293
294/// Set whether ime input is shown.
295pub fn set_ime_allowed(allowed: bool) {
296    add_update_message(UpdateMessage::SetImeAllowed { allowed });
297}
298
299/// Set the ime cursor area.
300pub fn set_ime_cursor_area(position: Point, size: Size) {
301    add_update_message(UpdateMessage::SetImeCursorArea { position, size });
302}
303
304/// Creates a new overlay on the current window.
305pub fn add_overlay<V: View + 'static>(view: V) -> ViewId {
306    let view = view.style(move |s| s.absolute()).into_any();
307    let id = view.id();
308
309    add_update_message(UpdateMessage::AddOverlay { view });
310    id
311}
312
313/// Removes an overlay from the current window.
314pub fn remove_overlay(id: ViewId) {
315    add_update_message(UpdateMessage::RemoveOverlay { id });
316}