191 lines
5.5 KiB
Markdown
191 lines
5.5 KiB
Markdown
Scriptable Event Handler with State<br/>JS Style
|
|
================================================
|
|
|
|
{{#include ../links.md}}
|
|
|
|
|
|
```admonish example
|
|
|
|
A runnable example of this implementation is included.
|
|
|
|
See the [_Examples_]({{rootUrl}}/start/examples/rust.md) section for details.
|
|
```
|
|
|
|
|
|
Keep State in Object Map
|
|
------------------------
|
|
|
|
This style allows defining new user state [variables] everywhere by packaging them all inside an
|
|
[object map], which is then exposed via the `this` pointer.
|
|
|
|
Because this scripting style resembles JavaScript, it is so named.
|
|
|
|
State variables can be freely created by _all_ [functions] (not just the `init` [function]).
|
|
|
|
The event handler type needs to hold this [object map] instead of a custom [`Scope`].
|
|
|
|
```rust
|
|
use rhai::{Engine, Scope, Dynamic, AST};
|
|
|
|
// Event handler
|
|
struct Handler {
|
|
// Scripting engine
|
|
pub engine: Engine,
|
|
// The custom 'Scope' can be used to hold global constants
|
|
pub scope: Scope<'static>,
|
|
// Use an object map (as a 'Dynamic') to keep stored state
|
|
pub states: Dynamic,
|
|
// Program script
|
|
pub ast: AST
|
|
}
|
|
```
|
|
|
|
|
|
Bind Object Map to `this` Pointer
|
|
---------------------------------
|
|
|
|
Initialization can simply be done via binding the [object map] containing global states to the
|
|
`this` pointer.
|
|
|
|
```rust
|
|
impl Handler {
|
|
// Create a new 'Handler'.
|
|
pub fn new(path: impl Into<PathBuf>) -> Self {
|
|
let mut engine = Engine::new();
|
|
|
|
:
|
|
// Code omitted
|
|
:
|
|
|
|
// Use an object map to hold state
|
|
let mut states = Map::new();
|
|
|
|
// Default states can be added
|
|
states.insert("bool_state".into(), Dynamic::FALSE);
|
|
|
|
// Convert the object map into 'Dynamic'
|
|
let mut states: Dynamic = states.into();
|
|
|
|
// Use 'call_fn_with_options' instead of 'call_fn' to bind the 'this' pointer
|
|
let options = CallFnOptions::new()
|
|
.eval_ast(false) // do not re-evaluate the AST
|
|
.rewind_scope(true) // rewind scope
|
|
.bind_this_ptr(&mut states); // bind the 'this' pointer
|
|
|
|
// In a real application you'd again be handling errors...
|
|
engine.call_fn_with_options(options, &mut scope, &ast, "init", ()).unwrap();
|
|
|
|
:
|
|
// Code omitted
|
|
:
|
|
|
|
Self { engine, scope, states, ast }
|
|
}
|
|
}
|
|
```
|
|
|
|
|
|
Bind `this` Pointer During Events Handling
|
|
------------------------------------------
|
|
|
|
Events handling should also use `Engine::call_fn_with_options` to bind the [object map] containing
|
|
global states to the `this` pointer via `CallFnOptions::this_ptr`.
|
|
|
|
```rust
|
|
pub fn on_event(&mut self, event_name: &str, event_data: i64) -> Dynamic {
|
|
let engine = &self.engine;
|
|
let scope = &mut self.scope;
|
|
let states = &mut self.states;
|
|
let ast = &self.ast;
|
|
|
|
let options = CallFnOptions::new()
|
|
.eval_ast(false) // do not re-evaluate the AST
|
|
.rewind_scope(true) // rewind scope
|
|
.bind_this_ptr(&mut states); // bind the 'this' pointer
|
|
|
|
match event_name {
|
|
// In a real application you'd be handling errors...
|
|
"start" => engine.call_fn_with_options(options, scope, ast, "start", (event_data,)).unwrap(),
|
|
:
|
|
:
|
|
}
|
|
}
|
|
```
|
|
|
|
|
|
Handler Scripting Style
|
|
-----------------------
|
|
|
|
```admonish note.side "No shadowing"
|
|
|
|
Notice that `this` can never be [shadowed][shadowing] because it is not a valid [variable] name.
|
|
```
|
|
|
|
Because the stored state is kept in an [object map], which in turn is bound to `this`, it is
|
|
necessary for [functions] to always access or modify these state [variables] via the `this` pointer.
|
|
|
|
As it is impossible to declare a local [variable] named `this`, there is no risk of accidentally
|
|
_[shadowing]_ a state [variable].
|
|
|
|
Because an [object map] is used to hold state values, it is even possible to add user-defined
|
|
[functions], leveraging the [OOP] support for [object maps].
|
|
|
|
### Sample script
|
|
|
|
```js
|
|
/// Initialize user-provided state.
|
|
/// State is stored inside an object map bound to 'this'.
|
|
fn init() {
|
|
// Can detect system-provided default states!
|
|
// Add 'bool_state' as new state variable if one does not exist
|
|
if "bool_state" !in this {
|
|
this.bool_state = false;
|
|
}
|
|
// Add 'obj_state' as new state variable (overwrites any existing)
|
|
this.obj_state = new_state(0);
|
|
|
|
// Can also add OOP-style functions!
|
|
this.log = |x| print(`State = ${this.obj_state.value}, data = ${x}`);
|
|
}
|
|
|
|
/// 'start' event handler
|
|
fn start(data) {
|
|
// Access state variables via 'this'
|
|
if this.bool_state {
|
|
throw "Already started!";
|
|
}
|
|
|
|
// New state variables can be created anywhere
|
|
this.start_mode = data;
|
|
|
|
if this.obj_state.func1() || this.obj_state.func2() {
|
|
throw "Conditions not yet ready to start!";
|
|
}
|
|
this.bool_state = true;
|
|
this.obj_state.value = data;
|
|
|
|
// Constant 'MY_CONSTANT' in custom scope is also visible!
|
|
print(`MY_CONSTANT = ${MY_CONSTANT}`);
|
|
}
|
|
|
|
/// 'end' event handler
|
|
fn end(data) {
|
|
if !this.bool_state || !("start_mode" in this) {
|
|
throw "Not yet started!";
|
|
}
|
|
if !this.obj_state.func1() && !this.obj_state.func2() {
|
|
throw "Conditions not yet ready to end!";
|
|
}
|
|
this.bool_state = false;
|
|
this.obj_state.value = data;
|
|
}
|
|
|
|
/// 'update' event handler
|
|
fn update(data) {
|
|
this.obj_state.value += process(data);
|
|
|
|
// Call user-defined function OOP-style!
|
|
this.log(data);
|
|
}
|
|
```
|