What is two-way data binding?

Two way data binding (at least Azul's definition of it) is when a component (for this examplelet's say a text field or a spreadsheet) can update it's own state (for example, to react tokey input or mouse events) without the user of the text field doing anything.

For example, a user could write a text field like this:

  1. struct DataModel {
  2. text_field_string: String,
  3. }
  4.  
  5. impl Layout for DataModel {
  6. fn layout(&self, _info: LayoutInfo<Self>) -> Dom<Self> {
  7. Dom::new(NodeType::Label(self.text_field_string))
  8. .with_callback(On::TextInput, Callback(update_text_field))
  9. }
  10. }
  11.  
  12. fn update_text_field(state: &mut AppState<DataModel>, event: &mut CallbackInfo<DataModel>) -> UpdateScreen {
  13. let ch = state.windows[event.window].get_keyboard_state().current_char?;
  14. state.modify(|state| state.text_field_string.push(ch));
  15. Redraw
  16. }

However, this has some serious drawbacks - the user of the library has to write theupdate_text_field callback in his application code, because the callback needs accessstate.text_field_string - and how could this be abstracted, you can't access <T>.text_field_stringbefore you know what the generic type T is going to be. Traditional toolkits use inheritance here -if the user derives from the TextInput class, then the callback can access <T>.text_field_string,but this also ties the inheriting class tightly to the TextInput (or use something likeObservableProperty in JavaFX). Dynamically typed languages can avoid this by essentially accessing fieldsthat don't exist at the time of writing the TextInput component, stringifying the fields of the component,accessing the field as T["text_field_string"]. This however, comes at a serious performance costand the loss of strict typing.

The second problem is that you'd have to write a new callback for each text field you add. If you'd wantto make a second text field, you'd have to make a second callback, or possibly hack around this with macros.There would be no way to abstract this further, so if that would be the final solution, everylibrary user would be required to copy-paste this code somewhere in his application in orderto get just one text field working. Luckily, there is a way around this, however, the implementor of theTextInput has to use unsafe code in order to implement it.

Important

The creator of the TextInput has to use unsafe code, but the user (the programmerwho instantiates the component in his application) does not. If certain guidelines are met(described further below), any component can expose a perfectly safe interface that can't be misused.

This document is only useful if you want to create custom components, not if you justwant to use them.

Writing a text input yourself

For this example, we'll write our own text input. Text input is the simplest way to explainand show how two-way data binding works. All standard-library widgets (such as Spreadsheetand Calendar) are implemented in the same way, there isn't any special-casing for standardwidgets - every "standard widget" is a custom component in itself (which is a part of whatmakes azul stand out in terms of composability).

Example code

Here is the full code for the text input we'll write. Without further explanation,just try to look at the code and see if you can figure out what it does:

  1. struct TextInput<T: Layout> {
  2. callback_id: DefaultCallbackId,
  3. marker: PhantomData<T>,
  4. }
  5.  
  6. pub struct TextInputState {
  7. pub text: String
  8. }
  9.  
  10. impl<T: Layout> TextInput<T> {
  11.  
  12. pub fn new(
  13. window: &mut FakeWindow<T>,
  14. state_to_bind: &TextInputState,
  15. full_data_model: &T)
  16. -> Self
  17. {
  18. let ptr = StackCheckedPointer::new(full_data_model, state_to_bind).unwrap();
  19. let callback_id = window.add_callback(ptr, DefaultCallback(Self::update_text_field));
  20. Self { callback_id, marker: PhantomData }
  21. }
  22.  
  23. pub fn dom(self, state_to_render: &TextInputState) -> Dom<T> {
  24. let mut container_div = Dom::new(NodeType::Div).with_class("text-input-container");
  25. container_div.add_default_callback_id(On::TextInput, self.callback_id);
  26. container_div.add_child(Dom::new(NodeType::Label(state_to_render.text.clone()));
  27. container_div
  28. }
  29.  
  30. fn update_text_field(
  31. data: &StackCheckedPointer<T>,
  32. app_state_no_data: AppStateNoData<T>,
  33. window_event: &mut CallbackInfo<T>
  34. ) -> UpdateScreen {
  35. unsafe { data.invoke_mut(TextInputState::update_state, app_state_no_data, window_event) }
  36. }
  37. }
  38.  
  39. impl TextInputState {
  40. pub fn update_state<T: Layout>(
  41. &mut self,
  42. app_state: AppStateNoData<T>,
  43. window_event: &mut CallbackInfo<T>)
  44. -> UpdateScreen
  45. {
  46. let ch = app_state.windows[event.window].get_keyboard_state().current_char?;
  47. self.text.push(ch);
  48. Redraw
  49. }
  50. }

And from the users side, here's how you'd instantiate and render a text input field that auto-updates thegiven TextInputState:

  1. struct MyAppData {
  2. my_text_input_1: TextInputState,
  3. }
  4.  
  5. impl Layout for MyAppData {
  6. fn layout(&self, info: LayoutInfo<Self>) -> Dom<Self> {
  7. TextInput::new(info.window, &self.my_text_input_1, &self).dom(&self.text_input)
  8. }
  9. }

A few things to note:

  • TextInput::updatetext_field() contains a line of unsafe code, but _it is a private function. This is good,because we know that any unsafe code mistakes can only happen inside of this module, not outside of it. If youwrite your own components, never make this function public.
  • TextInputState::update_text_field_inner() marked as public - this is important for delegating events,which we'll get into later on.
  • The function signatures for update_text_field() and update_state() are exactly the same, except forthe first argument.
  • add_default_callback_id() has an On::TextInput handler, meaning the update_text_field is only called when a TextInput event is emitted by the application user.
  • add_callback() requires a FakeWindow which the user can get from the LayoutInfo<T> during the DOMconstruction.
  • TextInput has a PhantomData<T> field, so that we can be sure that Dom<T>, DefaultCallback<T>and so on all use the same type for T.Now we managed to move the code from the user side into a reusable component, but how is it possiblethat the component can update its own state (which requires mutable access) while the application itselfis immutably borrowed? And what is StackCheckedPointer doing?

Stack-checked pointers

Updating state automatically requires us to have some form of mutable access to that state. However,we can't have any mutable pointers in the layout(&self) -> Dom<Self> function, because theapplication model is already mutably borrowed. But what Azul knows is that the DefaultCallbackisn't used immediately, it's only used once we actually invoke the callback, which can onlyhappen after the DOM construction is finished (because without a DOM, there would be no callbacks).

The next problem is that we can push a pointer into the DOM, but we can't store references to twodifferent types within the Dom. You could wrap everything in a Vec<Box<Any>>, and then call something likedom.add_callback(Box::new(text_input_state.clone())) - but then we would need to store theTextInputState inside the Dom which we don't want. What we'd technically want is aVec<Box<&Any>>, but then the problem is: how do we get the type of Box<&Any> back to aBox<&mut TextInputState>? And even if you could downcast a Box<&Any> to a Box<&TextInputState>, you'dstill need to cast the &TextInputState to a &mut TextInputState, in order to do anything with it -so the boxed trait does nothing for aliasing safety or type safety here. Boxed traits are a useful tool,but in this situation, they don't help with type safety at all - whether you cast void pointers ordowncast a Box<Any>, both solutions can crash at runtime, but the Box<Any> needs multiple layers ofindirection and creates problems with mutability.

The solution (and the unsafe part) is to require the programmer to push a &TextInputState into the Dom(which erases the original TextInputState type so that we can store multiple heterogenous types(&TextInputState, &CalendarState, &MyCoolComponent) inside a homogeneous Vec<StackCheckedPointer>.

Azul Default Callback - Registering a new default callback

The creator of the component has now to watch out for one thing - in the callback, he gets aStackCheckedPointer back - which is the same type-erased pointer that he pushed into the DOM earlier.Now the StackCheckedPointer can be casted back to a &TextInputState, which "reconstructs" the type:

Azul Default Callback - Invoking default callbacks

However, if we want to access the &TextInputState mutably - isn't &Thing as *const () as &mut Thingundefined behaviour? And how do we know that the pointer didn't point to the heap, and was erased (so thatwe don't have a dangling pointer to deleted memory)? To answer these questions, we have to take a look athow Azul wraps the data model and what is know about it:

Azul Default Callback Memory Model Stack Heap

Azul knows that the data model lives inside the App struct. So as long as the App is active, the lifetimefor the T is also valid. Second, Azul knows that any pointer to memory inside of the data model has to bein the range of &T as usize to &T as usize + sizeof::<T>() as usize. Meaning:

  1. use std::mem::size_of;
  2.  
  3. struct Something {
  4. a: u32,
  5. b: Vec<u32>,
  6. }
  7.  
  8. fn check_stack_or_heap<T, U>(haystack: &T, needle: &U) -> bool {
  9. needle as usize >= haystack as usize &&
  10. needle as usize + size_of::<U>() <= haystack as usize + size_of::<T>()
  11. }
  12.  
  13. fn main() {
  14. let something = Something { a: u32, b: vec![0, 1, 2, 3] };
  15.  
  16. // true: The address of `something` is 0x1234, the size of X is `0x1234 + 16 bytes`, so
  17. // `&something.a` will be at `0x1234 + 4 bytes`, which is in the range of the
  18. // `something` struct, therefore `something.a` is contained in the `something` struct.
  19. //
  20. // Ergo, the memory for `something.a` will live as long as `something` itself
  21. check_stack_or_heap(&something, &something.a);
  22.  
  23. // Also true: The vec itself (adress, len, capacity) is also stack-allocated and lives as
  24. // long as the `something` struct!
  25. check_stack_or_heap(&something, &something.b);
  26.  
  27. // False: `&something.b[3]` accesses the adress of a heap-allocated element.
  28. check_stack_or_heap(&something, &something.b[3]);
  29. }

The reason why azul can't allow heap-allocated pointers is fairly simple - what happens if a callback iscalled that removes the heap element that the const () points at? All subsequent callbacks wouldn'tknow about this change and would dereference an invalid pointer. The lifetime of the const () wouldn'tbe under the control of azul - while technically possible, it would be bound to be misused. However, thereis an easy workaround for this problem (for example, if you wanted to create multiple, heap-allocatedTextInputStates where you don't know how many you need at compile time?), which will be explained furtherdown the page.

Now what Azul knows about the *const () is that:

  • The pointer is inside the boundaries of T to T + sizeof::<T>() - as long as T is alive,the pointer points to valid memory._Note: this is not valid for enums, only structs, see#84 for soundness problems.
  • If we have unique mutable access to T, we also have unique mutable access to the pointer (sincewe checked that the pointer is a sub-part of the memory of T). Therefore, no aliasing occurs,therefore no undefined behaviour or race conditions are possible.The only thing that Azul doesn't know is the type of T. This would technically be solvable if Rustwould allow casting pointers via a TypeId (a unique ID that the compiler generates for each type),however, this isn't part of the Rust type system (and compiler) right now. So this work is the onlything that a programmer can potentially mess up:

Summary

If you use StackCheckedPointer::invoke_mut(), then you must make sure that theStackCheckedPointer gets casted to the same type that you originally pushed into the Dom.

Heap-allocated states

As mentioned earlier, what happens when you do want to create a variable number of TextInputStates?You can't stack-allocate them, because that wouldn't pass the StackCheckedPointer::new() test. The wayto solve this is to require a bit of help from the application programmer - first, instead of .unwrap()-ing the StackCheckedPointer, we simply don't push a DefaultCallback:

  1. struct TextInput<T: Layout> {
  2. // Make this optional!
  3. callback_id: Option<DefaultCallbackId>,
  4. marker: PhantomData<T>,
  5. }
  6.  
  7. impl<T: Layout> TextInput<T> {
  8.  
  9. pub fn new(
  10. window: &mut FakeWindow<T>,
  11. state_to_bind: &TextInputState,
  12. full_data_model: &T)
  13. -> Self
  14. {
  15. let callback_id = StackCheckedPointer::new(full_data_model, state_to_bind).and_then(|ptr| {
  16. window.add_callback(ptr, DefaultCallback(Self::update_text_field))
  17. });
  18. Self { callback_id, marker: PhantomData }
  19. }
  20.  
  21. pub fn dom(self, state_to_render: &TextInputState) -> Dom<T> {
  22. let mut container_div = Dom::new(NodeType::Div).with_class("text-input-container");
  23. if let Some(callback_id) = self.callback_id {
  24. container_div.add_default_callback_id(On::TextInput, self.callback_id);
  25. }
  26. container_div.add_child(Dom::new(NodeType::Label(state_to_render.text.clone()));
  27. container_div
  28. }
  29. }

Now, if the TextInputState is stack-allocated, everything works as expected, but if the TextInputStateis stored in a Vec - the field will still render, but not react to input events. The idea is the following:When an application programmer creates a Vec<TextInputState>, what he actually wants to know is what theindex of the hit item was. Note that we originally exposed the TextInputState::update_state as public,which is now important. So, a user could have a Vec<TextInputState> and then callmy_input_states[x].update_state() inside of a regular callback safely - without any unsafe code.

For this to work, you need to watch out for two things:

  • Only items that have any Callbacks or DefaultCallbacks attached to them get inserted into the list ofpotential hit-testable items. Since we can't call div.add_default_callback_id(), because we haveno DefaultCallbackId, we need some other way of telling azul that it should hit-test this item.
  • To solve the hit-testing situation, the application programmer needs to attach a callback to each one ofthe TextInputStates in the layout() function (attaching at least one callback to a div makes ithit-testable), then the callback can retrieve the index of the clicked TextInput by using CallbackInfo::get_index_in_parent().So the creator of the TextInput needs to make the child div hit-testable:
  1. if let Some(callback_id) = self.callback_id {
  2. container_div.add_default_callback_id(On::TextInput, self.callback_id);
  3. }

And the application programmer needs to remember that any heap-allocatedTextInputStates need to be hit-test seperately:

  1. struct MyApp {
  2. my_text_inputs: Vec<TextInputState>,
  3. }
  4.  
  5. impl Layout for DataModel {
  6. fn layout(&self, _info: LayoutInfo<Self>) -> Dom<Self> {
  7. // Tip: Dom<T> implements FromIterator - useful for lists and collections!
  8. my_text_inputs.iter().map(|text_input| {
  9. // Note: The "wrapper div" around the text input now has a callback
  10. // and all rendered text inputs now share the same callback,
  11. // the .bind() method doesn't need to be called because it wouldn't
  12. // succeed anyway (since the TextInputs are on the heap)
  13. TextInput::new(info.window, &self.my_text_input_1, &self).dom()
  14. .with_callback(On::TextInput, Callback(update_all_the_text_fields)))
  15. }).collect()
  16. }
  17. }
  18.  
  19. // Calls the public `TextInputState::update_state` function on the correct TextInput
  20. fn update_all_the_text_fields(state: &mut AppState<DataModel>, event: &mut CallbackInfo<DataModel>) -> UpdateScreen {
  21. let (child_idx, _parent_node_id) = event.get_index_in_parent(event.hit_dom_node)?;
  22. state.data.lock().ok()?.my_text_inputs[child_idx].update_state(state.without_data(), event)
  23. }

Sidenote: Earlier versions of azul allowed you to make the parent hit-testable and the get the index of thechild from a callback attached to the parent. This was impractical, because the child is hierarchicallyinside of the parent, but often not visually (i.e. absolute positioned children that aren't "inside" oftheir parents area). This is why the callback has to be attached to all TextInputs instead of the parent container.

Of course, you can wrap all of this in another (stack-allocated) component, i.e. TextInputListComponent andmanage the delegation of callbacks inside the TextInputListComponent::dom() function, for example. This allowsyou to create reusable components and callbacks for your custom components and reuse these components as plug-insfrom external libraries.

Summary

In this chapter you have learned:

  • Why the callback model is slightly more complicated than in other frameworks
  • Why a text input suddenly stops working if you put it on the heap instead of the stack
  • What to watch out for when implementing custom components
  • How to create heap-allocated lists of custom components and work around the current limitations