[TODO] Elements
For most of the Elements a standalone implementation would not make a lot of sense due to the complexity of the user application states. Therefore the library should instead provide small examples to show how you can implement such user fields yourself and connect them using your own event system loops to communicate with other Containers and/or Elements.
Currently the only library provided Element implementation is the Scrollable element.
Scrollable
Contents that is scrollable should be done virtually through the contents of the Container. This means each container contents implements scrolling for itself if required. Provided with a minimal size option the corresponding container becomes scrollable if the actual containing Container is too small to hold the minimal size. Otherwise the Container is not scrollable (as it is not necessary). With this the Container is dynamically scrollable if necessary (i.e. if the screen size changes, etc.).
Known issues:
- The scrolling input action would then also be implemented by the user, but how given that some if the user input is already handled (i.e. scrolling with the mouse).
- To be implemented: Automatic rendering of a scrollbar (vertical and/or horizontal - according to the container size and the virtual size).
- Nested scrollable
Containerhave not been tested and may not work as expected.
Template
The following snippet shows a template for an Element implementation:
/// This is an empty template implementation for an `Element` type.
pub fn Template(Model: type, Event: type) type {
// *Model* should be `App.Model`
// *Event* should be `App.Event`
return struct {
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.resize = resize,
.reposition = reposition,
.handle = handle,
.content = content,
},
};
}
fn resize(ctx: *anyopaque, size: Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
_ = this;
_ = size;
}
fn reposition(ctx: *anyopaque, origin: Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
_ = this;
_ = origin;
}
fn handle(ctx: *anyopaque, model: *Model, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
_ = this;
_ = model;
switch (event) {
else => {},
}
}
fn content(ctx: *anyopaque, model: *const Model, cells: []Cell, size: Size) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
_ = this;
_ = model;
}
};
}
Tip
For example implementations of
Elementtypes, please refer to examples.
Tips
Only provide function callbacks for the Element implementation if the callback is necessary for the functionality, otherwise do not provide a callback function in the vtable when creating an Element instance of the implementation.
The element initializer function should be the only public function of the Element implementation, as the cast of the *anyopaque pointer will provide you will an instance of the specific implementation, giving you access to the private functions, members, etc. anyway.
The documentation contains tips about how to implement corresponding event loops or how to design own Elements. And also on how to test accordingly and use the library itself for examples on how to design the test cases.
User specific event handling and content rendering
For interactions controlled by the user each container can use an Element interface which contains functions which are called by the Container during event handling (i.e. fn handle(..)) and during rendering (i.e. fn content(..)) to provide user specific content and user interaction. The Element may be stateful, but may also be stateless and then be re-used in multiple different Containers.
Composing multiple Elements currently requires the implementation of a wrapper which contains the Elements that need to be handled (should work pretty well for stateless Elements). Such stateless Elements may be provided by this library.
Stateful Elements
You can capture state in two different places:
- On a per
Elementinstance basis through member variables of a given instance. - In the
Modelthat is shared accross the entire application.
State using the first idea should be keept at a minimum and used if they are specific to only that Element implementation and no others in your application.
State that shall be shared between other Elements in your application should propagate their state to the Model.
Warning
However keep in might that this might cause side-effects (i.e. having the same
Elementimplementation instanciated multiple times in the sameContainertree, might cause thehandlecallback to be called multiple times in a single event loop iteration).
Note
This exact seperatation is also used in android app development using Jetpack compose. Common patterns, best practices, etc. may also apply for zterm.