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