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]&lt;dyn Widget&gt;</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}