all_is_cubes_ui/vui/widget_trait.rs
1//! UI [`Widget`] trait and related glue.
2
3#![allow(
4 unused_assignments,
5 reason = "nightly FP <https://github.com/rust-lang/rust/issues/147648>"
6)]
7
8use alloc::boxed::Box;
9use alloc::sync::Arc;
10use core::error::Error;
11use core::fmt::Debug;
12
13use bevy_platform::sync::Mutex;
14
15use all_is_cubes::behavior::{self, Behavior};
16use all_is_cubes::math::GridAab;
17use all_is_cubes::space::{self, Space, SpaceTransaction};
18use all_is_cubes::time::Tick;
19use all_is_cubes::transaction::{self, Merge as _};
20use all_is_cubes::universe::{HandleVisitor, ReadTicket, UniverseTransaction, VisitHandles};
21
22// reused for WidgetController
23pub use all_is_cubes::behavior::Then;
24
25use crate::vui::{LayoutGrant, Layoutable, Positioned, validate_widget_transaction};
26
27/// Transaction type produced by [`WidgetController`]s.
28/// Placeholder for likely wanting to change this later.
29pub type WidgetTransaction = SpaceTransaction;
30
31/// Something that can participate in UI layout (via [`Layoutable`]), and then turn into
32/// some interactive contents of a [`Space`] (via [`controller()`]) positioned according
33/// to that layout.
34///
35/// This trait is object-safe so that collections (and in particular [`LayoutTree`]s) of
36/// <code>[Arc]<dyn Widget></code> can be used.
37///
38/// # Mutability and dependence
39///
40/// A widget may be instantiated in multiple [`Space`]s by calling [`controller()`] more
41/// than once. All such instances should operate independently (not interfering with each
42/// other) and equivalently.
43///
44/// A widget may reference changing data (e.g. the current value of some setting).
45/// However, a widget should not behave differently in response to such data, as the
46/// systems which manage widgets do not track such changes.
47/// In particular, the widget's implementation of [`Layoutable`] should always give the
48/// same answer, and the [`WidgetController`] it produces should be equivalent to instances
49/// created at other times.
50/// Instead, the [`WidgetController`], once created, is responsible for updating the
51/// particular piece of [`Space`] granted to the widget whenever the input data changes
52/// or the widget is interacted with.
53///
54/// # Where to find widgets
55///
56/// Standard widgets may be found in the [`vui::widgets`](crate::vui::widgets) module.
57///
58/// [`LayoutTree`]: crate::vui::LayoutTree
59/// [`controller()`]: Self::controller
60pub trait Widget: Layoutable + Debug + Send + Sync {
61 /// Create a [`WidgetController`] to manage the widget's existence in a particular
62 /// region of a particular [`Space`].
63 ///
64 /// The difference between a [`Widget`] and its [`WidgetController`]s is that each
65 /// [`WidgetController`] must *separately* keep track of which changes need to be
66 /// performed within its associated [`Space`]; the [`Widget`] may be instantiated in
67 /// any number of [`Space`]s but does not need to keep track of them all. It is
68 /// common for a [`WidgetController`] to hold an [`Arc`] pointer to its [`Widget`] to
69 /// make use of information from its original definition.
70 ///
71 /// You should not usually need to call this method, but rather use
72 /// [`WidgetTree::installation()`](crate::vui::LayoutTree::installation) to create
73 /// controllers and attach them to a [`Space`]. However, it is valid for a widget to
74 /// reuse another widget's controller implementation.
75 fn controller(self: Arc<Self>, grant: &LayoutGrant) -> Box<dyn WidgetController>;
76}
77
78/// Does the work of making a particular region of a [`Space`] behave as a particular
79/// [`Widget`].
80///
81/// Instances of [`WidgetController`] are obtained by calling [`Widget::controller()`].
82/// In most cases, [`Widget`] implementations have corresponding [`WidgetController`]
83/// implementations, though there are common utilities such as [`OneshotController`].
84///
85/// Currently, [`WidgetController`]s are expected to manage their state and todo by being
86/// mutable — unlike the normal [`Behavior`] contract. This has been chosen as an acceptable
87/// compromise for convenience because controllers are required not to operate outside
88/// their assigned regions of space and therefore will not experience transaction conflicts.
89///
90/// [`OneshotController`]: crate::vui::widgets::OneshotController
91pub trait WidgetController: Debug + VisitHandles + Send + Sync + 'static {
92 /// Write the initial state of the widget to the space.
93 /// This is called at most once.
94 fn initialize(
95 &mut self,
96 context: &WidgetContext<'_, '_>,
97 ) -> Result<WidgetTransaction, InstallVuiError> {
98 let _ = context;
99 Ok(WidgetTransaction::default())
100 }
101
102 /// For widgets which are installed in a session's UI universe,
103 /// this is called every frame before [`Self::step()`] to allow the widget controller to fetch
104 /// information from the game universe via the provided [`ReadTicket`].
105 ///
106 /// If this is not overridden, it will do nothing.
107 ///
108 /// TODO: This is a kludge which should go away soon along with substantial refactoring of
109 /// the UI as a whole.
110 fn synchronize(&mut self, world_read_ticket: ReadTicket<'_>, ui_read_ticket: ReadTicket<'_>) {
111 _ = world_read_ticket;
112 _ = ui_read_ticket;
113 }
114
115 /// Called every frame (except as [`Then`] specifies otherwise)
116 /// to update the state of the space to match the current state of
117 /// the widget's data sources or user interaction.
118 ///
119 /// If this is not overridden, it will do nothing and the controller will be dropped.
120 fn step(&mut self, context: &WidgetContext<'_, '_>) -> Result<StepSuccess, StepError> {
121 let _ = context;
122 Ok((WidgetTransaction::default(), Then::Drop))
123 }
124}
125
126/// Successful return of [`WidgetController::step()`].
127///
128/// The [`Then`] determines when `step()` is called again, if it is.
129///
130/// TODO: This should become a struct that will allow more extensibility for future needs.
131pub type StepSuccess = (WidgetTransaction, Then);
132
133/// Error return of [`WidgetController::step()`].
134///
135/// TODO: This should become a more specific error type.
136pub type StepError = Box<dyn Error + Send + Sync>;
137
138impl WidgetController for Box<dyn WidgetController> {
139 fn synchronize(&mut self, world_read_ticket: ReadTicket<'_>, ui_read_ticket: ReadTicket<'_>) {
140 (**self).synchronize(world_read_ticket, ui_read_ticket)
141 }
142
143 fn step(&mut self, context: &WidgetContext<'_, '_>) -> Result<StepSuccess, StepError> {
144 (**self).step(context)
145 }
146
147 fn initialize(
148 &mut self,
149 context: &WidgetContext<'_, '_>,
150 ) -> Result<WidgetTransaction, InstallVuiError> {
151 (**self).initialize(context)
152 }
153}
154
155/// Wraps a [`WidgetController`] to make it into a [`Behavior`].
156// TODO: Eliminate this iff it doesn't continue to be a useful abstraction.
157// TODO: This uses interior mutability when it shouldn't (behaviors are supposed
158// to mutate self via transaction); is that fine? It'll certainly mean that failing
159// transactions might be lost, but that might be as good as anything.
160#[derive(Debug)]
161pub(super) struct WidgetBehavior {
162 /// Original widget -- not used directly but for error reporting
163 widget: Positioned<Arc<dyn Widget>>,
164 controller: Mutex<Box<dyn WidgetController>>,
165}
166
167impl WidgetBehavior {
168 /// Returns a transaction which adds the given widget controller to the space,
169 /// or an error if the controller's `initialize()` fails.
170 pub(crate) fn installation(
171 widget: Positioned<Arc<dyn Widget>>,
172 mut controller: Box<dyn WidgetController>,
173 read_ticket: ReadTicket<'_>,
174 ) -> Result<SpaceTransaction, InstallVuiError> {
175 let init_txn = match controller.initialize(&WidgetContext {
176 read_ticket,
177 behavior_context: None,
178 grant: &widget.position,
179 }) {
180 Ok(t) => t,
181 Err(e) => {
182 return Err(InstallVuiError::WidgetInitialization {
183 widget: controller,
184 error: Box::new(e),
185 });
186 }
187 };
188 let add_txn = behavior::BehaviorSetTransaction::insert(
189 // TODO: widgets should be rotatable and that should go here
190 space::SpaceBehaviorAttachment::new(widget.position.bounds),
191 Arc::new(WidgetBehavior {
192 widget,
193 controller: Mutex::new(controller),
194 }),
195 );
196 init_txn
197 .merge(SpaceTransaction::behaviors(add_txn))
198 .map_err(|error| InstallVuiError::Conflict { error })
199 }
200}
201
202impl VisitHandles for WidgetBehavior {
203 fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
204 let Self {
205 widget: _,
206 controller,
207 } = self;
208 // Not visiting the widget because it is not used for actual behavior
209 // (no handles it may contain will be used *by* this WidgetBehavior).
210 controller.lock().unwrap().visit_handles(visitor);
211 }
212}
213
214impl Behavior<Space> for WidgetBehavior {
215 fn step(&self, context: &behavior::Context<'_, '_, Space>) -> (UniverseTransaction, Then) {
216 let (txn, then) = self
217 .controller
218 .lock()
219 .unwrap()
220 .step(&WidgetContext {
221 read_ticket: context.read_ticket,
222 behavior_context: Some(context),
223 grant: &self.widget.position,
224 })
225 .expect("TODO: behaviors should have an error reporting path");
226 // TODO: should be using the attachment bounds instead of the layout grant to validate bounds
227 validate_widget_transaction(&self.widget.value, &txn, &self.widget.position)
228 .expect("transaction validation failed");
229 (context.bind_host(txn), then)
230 }
231
232 fn persistence(&self) -> Option<behavior::Persistence> {
233 None
234 }
235}
236
237/// Context passed to [`WidgetController::step()`].
238#[derive(Debug)]
239pub struct WidgetContext<'ctx, 'read> {
240 behavior_context: Option<&'ctx behavior::Context<'ctx, 'read, Space>>,
241 /// [`ReadTicket`] for the universe the widget is UI for, not the one it is in.
242 read_ticket: ReadTicket<'ctx>,
243 grant: &'ctx LayoutGrant,
244}
245
246impl<'a> WidgetContext<'a, '_> {
247 /// The time tick that is currently passing, causing this step.
248 pub fn tick(&self) -> Tick {
249 match self.behavior_context {
250 Some(context) => context.tick,
251 None => {
252 // In this case we are initializing the widget
253 // TODO: This violates Tick::from_paused's documented "This should only be used in tests"
254 Tick::from_seconds(0.0)
255 }
256 }
257 }
258
259 /// Returns the [`LayoutGrant`] given to this widget; the same value as when
260 /// [`Widget::controller()`] was called.
261 pub fn grant(&self) -> &'a LayoutGrant {
262 self.grant
263 }
264
265 #[allow(dead_code)] // TODO(read_ticket): this may or may not end up being needed
266 pub(crate) fn read_ticket(&self) -> ReadTicket<'a> {
267 self.read_ticket
268 }
269}
270
271/// Errors that may arise from setting up [`LayoutTree`]s and [`Widget`]s and installing
272/// them in a [`Space`].
273///
274/// [`LayoutTree`]: crate::vui::LayoutTree
275#[derive(Debug, displaydoc::Display)]
276#[non_exhaustive]
277pub enum InstallVuiError {
278 /// The widget failed to initialize for some reason.
279 #[displaydoc("error initializing widget ({widget:?})")]
280 WidgetInitialization {
281 /// TODO: This should be `Arc<dyn Widget>` instead.
282 /// Or, if we come up with some way of giving widgets IDs, that.
283 widget: Box<dyn WidgetController>,
284
285 /// The error returned by [`WidgetController::initialize()`].
286 error: Box<InstallVuiError>,
287 },
288
289 /// A transaction conflict arose between two widgets or parts of a widget's installation.
290 #[displaydoc("transaction conflict involving a widget")]
291 #[non_exhaustive]
292 Conflict {
293 // TODO: Include the widget(s) involved, once `Arc<dyn Widget>` is piped around everywhere
294 // and not just sometimes Widget or sometimes WidgetController.
295 //
296 // TODO: Now that `ExecuteError` contains conflicts, we should consider making this a
297 // sub-case of `ExecuteInstallation`.
298 #[allow(missing_docs)]
299 error: space::SpaceTransactionConflict,
300 },
301
302 /// The widget attempted to modify space outside its assigned bounds.
303 #[displaydoc(
304 "widget attempted to write out of bounds\n\
305 grant: {grant:?}\n\
306 attempted write: {erroneous:?}\n\
307 widget: {widget:?}\n\
308 "
309 )]
310 OutOfBounds {
311 /// The region given to the widget.
312 grant: LayoutGrant,
313
314 /// The region the widget attempted to modify.
315 erroneous: GridAab,
316
317 /// The widget.
318 widget: Arc<dyn Widget>,
319 },
320
321 /// Installing the widget tree failed, because one of the widgets' transactions failed.
322 /// This usually indicates a bug in the widget implementation.
323 #[displaydoc("installing widget tree failed")]
324 #[non_exhaustive]
325 ExecuteInstallation {
326 #[allow(missing_docs)]
327 error: transaction::ExecuteError<SpaceTransaction>,
328 },
329}
330
331impl Error for InstallVuiError {
332 fn source(&self) -> Option<&(dyn Error + 'static)> {
333 match self {
334 InstallVuiError::WidgetInitialization { error, .. } => Some(error),
335 InstallVuiError::Conflict { error } => Some(error),
336 InstallVuiError::OutOfBounds { .. } => None,
337 InstallVuiError::ExecuteInstallation { error } => Some(error),
338 }
339 }
340}
341
342impl From<InstallVuiError> for all_is_cubes::linking::InGenError {
343 fn from(value: InstallVuiError) -> Self {
344 all_is_cubes::linking::InGenError::other(value)
345 }
346}
347
348/// Calls [`WidgetController::synchronize()`] on every widget installed in the space.
349#[cfg_attr(not(feature = "session"), allow(dead_code))]
350pub(crate) fn synchronize_widgets(
351 world_read_ticket: ReadTicket<'_>,
352 ui_read_ticket: ReadTicket<'_>,
353 space: &space::Read<'_>,
354) {
355 for item in space.behaviors().query::<WidgetBehavior>() {
356 item.behavior
357 .controller
358 .lock()
359 .unwrap()
360 .synchronize(world_read_ticket, ui_read_ticket);
361 }
362}
363
364/// Create a [`Space`] to put a widget in.
365#[cfg(test)]
366#[track_caller]
367pub(crate) fn instantiate_widget<W: Widget + 'static>(
368 read_ticket: ReadTicket<'_>,
369 grant: LayoutGrant,
370 widget: W,
371) -> (Option<GridAab>, Space) {
372 use crate::vui;
373 use all_is_cubes::transaction::Transaction as _;
374
375 let mut space = Space::builder(grant.bounds).build();
376 let txn = vui::install_widgets(grant, &vui::leaf_widget(widget), read_ticket)
377 .expect("widget instantiation");
378 let bounds = txn.bounds_only_cubes();
379 txn.execute(&mut space, read_ticket, &mut transaction::no_outputs)
380 .expect("widget transaction");
381 (bounds, space)
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use all_is_cubes::util::assert_conditional_send_sync;
388
389 #[test]
390 fn error_is_send_sync() {
391 assert_conditional_send_sync::<InstallVuiError>()
392 }
393
394 fn _assert_widget_trait_is_object_safe(_: &dyn Widget) {}
395 fn _assert_controller_trait_is_object_safe(_: &dyn WidgetController) {}
396}