Skip to main content

qframe/runtime/
command.rs

1//! Work an application asks the runtime to do after an update.
2
3use super::confirm::Confirm;
4use super::task::{Task, TaskId};
5use crate::icons::IconMode;
6use crate::widgets::{Corner, Toast};
7
8pub(crate) enum Action<Msg> {
9    Quit,
10    Focus(String),
11    SetTheme(String),
12    SetLocale(String),
13    SetIconMode(IconMode),
14    SetReducedMotion(bool),
15    SetPillar(crate::icons::PillarStyle),
16    SetSlide(bool),
17    Copy(String),
18    Confirm(Confirm<Msg>),
19    ReadClipboard(Box<dyn FnOnce(Option<String>) -> Msg>),
20    Perform(Box<dyn FnOnce() -> Msg + Send>),
21    Toast(Toast<Msg>),
22    DismissToast(String),
23    ToastCorner(Corner),
24    Task(Task<Msg>),
25    CancelTask(TaskId),
26}
27
28/// Work for the runtime, returned from [`App::update`](crate::runtime::App::update).
29pub struct Command<Msg> {
30    pub(crate) actions: Vec<Action<Msg>>,
31}
32
33impl<Msg: Send + 'static> Command<Msg> {
34    /// Nothing to do.
35    #[must_use]
36    pub fn none() -> Self {
37        Self { actions: Vec::new() }
38    }
39
40    /// Several commands, run in order.
41    #[must_use]
42    pub fn batch(commands: impl IntoIterator<Item = Self>) -> Self {
43        Self { actions: commands.into_iter().flat_map(|command| command.actions).collect() }
44    }
45
46    /// Leaves the application after this update.
47    #[must_use]
48    pub fn quit() -> Self {
49        Self::single(Action::Quit)
50    }
51
52    /// Moves keyboard focus to the widget named `name` with [`NodeMut::id`](crate::widget::NodeMut::id).
53    /// When no such widget is on screen yet, focus moves to it after the next frame if it
54    /// appears there, so an update can show a widget and focus it at once.
55    #[must_use]
56    pub fn focus(name: impl Into<String>) -> Self {
57        Self::single(Action::Focus(name.into()))
58    }
59
60    /// Switches to theme `id`. An unusable theme falls back to the default and is reported in
61    /// the environment's diagnostics.
62    #[must_use]
63    pub fn set_theme(id: impl Into<String>) -> Self {
64        Self::single(Action::SetTheme(id.into()))
65    }
66
67    /// Switches the language to locale `code`.
68    #[must_use]
69    pub fn set_locale(code: impl Into<String>) -> Self {
70        Self::single(Action::SetLocale(code.into()))
71    }
72
73    /// Switches between Nerd Font, Unicode, ASCII or detected glyphs.
74    #[must_use]
75    pub fn set_icon_mode(mode: IconMode) -> Self {
76        Self::single(Action::SetIconMode(mode))
77    }
78
79    /// Turns reduced motion on or off: layers appear at once and nothing breathes or spins.
80    /// Has no effect while the `QUVYTA_REDUCED_MOTION` environment variable decides.
81    #[must_use]
82    pub fn set_reduced_motion(reduced: bool) -> Self {
83        Self::single(Action::SetReducedMotion(reduced))
84    }
85
86    /// Draws every pillar in `style` over the theme's choice.
87    #[must_use]
88    pub fn set_pillar(style: crate::icons::PillarStyle) -> Self {
89        Self::single(Action::SetPillar(style))
90    }
91
92    /// Turns the one-cell slide of hovered and selected entries in list structures (lists, menus,
93    /// trees, tables, tab rails, setting rows, dropdown options and tabs) on or off over the
94    /// theme's `motion.slide`. Buttons and other controls never slide.
95    #[must_use]
96    pub fn set_slide(slide: bool) -> Self {
97        Self::single(Action::SetSlide(slide))
98    }
99
100    /// Copies `text` to the system clipboard (OSC 52, which also works over SSH) and to the
101    /// application's in-process clipboard, which keeps pasting inside the application working on
102    /// terminals without OSC 52.
103    #[must_use]
104    pub fn copy(text: impl Into<String>) -> Self {
105        Self::single(Action::Copy(text.into()))
106    }
107
108    /// Reads the clipboard and delivers its text, or `None` when there is none. Like the `paste`
109    /// key and Paste menu entries it tries, in order: the system clipboard through its tool
110    /// (`wl-paste`, `xclip` or `xsel`, `pbpaste`; run without a shell, briefly, off the drawing
111    /// thread), the terminal's clipboard through an OSC 52 query (many terminals do not answer,
112    /// so the wait is short), and the text copied last inside this application (by a widget, a
113    /// selection or [`Command::copy`]). The message arrives in a later update once the text is
114    /// known; drawing never waits for it. Text pasted with the terminal's own paste arrives as
115    /// [`Event::Paste`](crate::event::Event::Paste) instead.
116    #[must_use]
117    pub fn read_clipboard(message: impl FnOnce(Option<String>) -> Msg + 'static) -> Self {
118        Self::single(Action::ReadClipboard(Box::new(message)))
119    }
120
121    /// Runs `work` on a background thread and delivers its message when done. Drawing never
122    /// waits for it.
123    #[must_use]
124    pub fn perform(work: impl FnOnce() -> Msg + Send + 'static) -> Self {
125        Self::single(Action::Perform(Box::new(work)))
126    }
127
128    /// Asks the user a question in a dialog the runtime shows over the application, and
129    /// delivers the message of their answer: the confirm message, or the cancel message (if
130    /// any) for Cancel, Esc and the close mark. Cancel, the safe answer, has focus when the dialog opens.
131    /// Several requests stack; the newest is answered first.
132    ///
133    /// ```
134    /// use qframe::prelude::*;
135    ///
136    /// enum Msg {
137    ///     AskRemove,
138    ///     Remove,
139    /// }
140    ///
141    /// fn update(msg: Msg) -> Command<Msg> {
142    ///     match msg {
143    ///         Msg::AskRemove => Command::confirm(
144    ///             Confirm::new("Remove container?", Msg::Remove).message("Its volumes are deleted too.").danger(),
145    ///         ),
146    ///         Msg::Remove => Command::none(),
147    ///     }
148    /// }
149    /// ```
150    #[must_use]
151    pub fn confirm(confirm: Confirm<Msg>) -> Self {
152        Self::single(Action::Confirm(confirm))
153    }
154
155    /// Shows `toast` in the toast corner, above everything else. It slides in, stays for its
156    /// duration (paused while the pointer is on it) and slides out; a click on its close mark dismisses it.
157    #[must_use]
158    pub fn toast(toast: Toast<Msg>) -> Self {
159        Self::single(Action::Toast(toast))
160    }
161
162    /// Removes the toast shown with [`Toast::key`] `key`.
163    #[must_use]
164    pub fn dismiss_toast(key: impl Into<String>) -> Self {
165        Self::single(Action::DismissToast(key.into()))
166    }
167
168    /// Stacks toasts in `corner` from now on; bottom right by default.
169    #[must_use]
170    pub fn toast_corner(corner: Corner) -> Self {
171        Self::single(Action::ToastCorner(corner))
172    }
173
174    /// Starts `task` on a background thread. Its `Started` event is applied before this update
175    /// returns; progress, messages and the outcome arrive as the work goes on.
176    #[must_use]
177    pub fn task(task: Task<Msg>) -> Self {
178        Self::single(Action::Task(task))
179    }
180
181    /// Asks task `id` to stop: its sleeps wake at once, [`TaskCx::is_cancelled`](crate::runtime::TaskCx::is_cancelled)
182    /// turns true and it ends as [`TaskOutcome::Cancelled`](crate::runtime::TaskOutcome::Cancelled).
183    /// Asking a finished task does nothing.
184    #[must_use]
185    pub fn cancel_task(id: TaskId) -> Self {
186        Self::single(Action::CancelTask(id))
187    }
188
189    fn single(action: Action<Msg>) -> Self {
190        Self { actions: vec![action] }
191    }
192}