update documentation for design, elements, roadmap and testing

2025-03-01 16:17:55 +01:00
parent 528079a96c
commit 43f3c8ab1a
4 changed files with 162 additions and 72 deletions

@@ -5,7 +5,7 @@ This project draws heavy inspiration from [clay](https://github.com/nicbarker/cl
> [!NOTE] > [!NOTE]
> All examples are implemented as *intermediate mode* rendered applications. > All examples are implemented as *intermediate mode* rendered applications.
There is only one generic `Container` which includes properties, child container and optionally an `Element`. In the end a `Container` describes a tree structure containing layout and content. There is only one generic `Container` which includes properties, child container and optionally an `Element`. In the end a `Container` describes a tree structure containing layout and content. [Elements](Elements.md) describe user defined content and behavior building on top of the existing `Container` layouting.
The library is designed to be very basic and *not to provide* any more complex elements such as input fields, drop-down menu's, buttons, etc. Some of them are either easy to implement yourself, specific for you needs or a too complex to be provided by the library effectively. For these use-cases there may be other libraries that build on top of this one to provide the complex elements as some sort of pre-built elements for you to use in your application (or you create them yourself). The library is designed to be very basic and *not to provide* any more complex elements such as input fields, drop-down menu's, buttons, etc. Some of them are either easy to implement yourself, specific for you needs or a too complex to be provided by the library effectively. For these use-cases there may be other libraries that build on top of this one to provide the complex elements as some sort of pre-built elements for you to use in your application (or you create them yourself).

@@ -1,14 +1,22 @@
## [TODO] Elements ## [TODO] Elements
The documentation may contain tips about how to implement corresponding event loops or how to design own `Element`s. And also on how to test accordingly and use the library itself for examples on how to design the test cases. For most of the `Element`s 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 `Container`s and/or `Element`s.
For most of the `Element`s a standalone implementation would not make a lot Currently the only library provided `Element` implementation is the `Scrollable` element.
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 `Container`s and/or `Element`s.
## Template ### 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 `Container` have not been tested and may not work as expected.
### Template
The following snippet shows a template for an `Element` implementation:
```zig ```zig
/// This is an empty template implementation for an `Element` type. /// This is an empty template implementation for an `Element` type.
@@ -41,39 +49,15 @@ pub fn Template(Event: type) type {
} }
``` ```
For an example implementations of `Element` types, please refer to [examples](Examples.md).
### Tips
The documentation contains tips about how to implement corresponding event loops or how to design own `Element`s. 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 ### User specific event handling and content rendering
For interactions controlled by the user each container can use an `Element` 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 `Container`s.
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 `Container`s.
Composing multiple `Element`s currently requires the implementation of a wrapper Composing multiple `Element`s currently requires the implementation of a wrapper which contains the `Element`s that need to be handled (should work pretty well for stateless `Element`s). Such *stateless* `Element`s may be provided by this library.
which contains the `Element`s that need to be handled (should work pretty well
for stateless `Element`s). Such *stateless* `Element`s may be provided by this
library.
### 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.
This still has one issue: Layout of child elements that are already too large
(i.e. or become too small). The library could provide automatic rendering of a
scrollbar given the right parameters however. The scrolling input action would
then also be implemented by the user.
Open questions are regarding the sizing options (i.e. how is the size of a
`Container` actually controlled?, how should it be controlled?, etc.). There
should be support for the child elements to provide some kind of 'list'
functionality built-in.
**REMINDER**: (mostly for myself) The library should be and remain simple. This
means that some code for using the library may be duplicated, but this is not
the main goal. Others may provide more re-usable code snippets that build on top
of this library instead.
### Advanced usage

@@ -3,21 +3,21 @@
The following list contains goals for certain features the library supports or is planned on supporting. The following list contains goals for certain features the library supports or is planned on supporting.
- [ ] Container rendering - [ ] Container rendering
- [ ] Layout - [x] Layout
- [x] direction - [x] direction
- [x] vertical - [x] vertical
- [x] horizontal - [x] horizontal
- [x] padding - [x] padding
- [x] gap - [x] gap
- [ ] sizing
- [ ] max size
- [x] min size (provide size to use which would be a minimal size - as if the actual size is smaller then the `Container` will scroll and otherwise the contents expand to the available space instead?)
- [x] Border - [x] Border
- [x] sides - [x] sides
- [x] corners - [x] corners
- [x] separators - [x] separators
- [x] Rectangle - [x] Rectangle
- [x] min size - [ ] Sizing options (should be only one option at any given time possible)
- [x] fixed size (*absolute*)
- [ ] percentage size (*relative*)
- [ ] Inline rendering (**later**)
- [ ] User control - [ ] User control
- [x] event loop handling - [x] event loop handling
- [x] mouse support - [x] mouse support
@@ -32,11 +32,13 @@ The following list contains goals for certain features the library supports or i
- [ ] vertical - [ ] vertical
- [ ] horizontal - [ ] horizontal
- [ ] Content alignment (i.e. standard calculations done with the provided `Size`) - [ ] Content alignment (i.e. standard calculations done with the provided `Size`)
- [ ] horizontal center (i.e. calculate the corresponding anchor)
- [ ] vertical center (i.e. calculate the corresponding anchor)
- [x] User input - [x] User input
- [x] single line - [x] single line
- [x] multi line - [x] multi line
- [ ] image support through kitty protocol (**later**) - [ ] image support through kitty protocol (**later**)
- [ ] Inline rendering (**later**) - [ ] embeeding another application (i.e. an editor, or something like that)? (**later**)
- [ ] Examples - [ ] Examples
- [x] Layouts - [x] Layouts
- [x] vertical - [x] vertical
@@ -74,6 +76,9 @@ The following list contains goals for certain features the library supports or i
- [x] use another tui application to launch and come back to (showcase the interrupt behavior) - [x] use another tui application to launch and come back to (showcase the interrupt behavior)
- [x] Launch sub-applications (not inside of a `Container` but during the application workflow, like an editor) - [x] Launch sub-applications (not inside of a `Container` but during the application workflow, like an editor)
- [ ] implement some functionality to have it be more like a working demo application - [ ] implement some functionality to have it be more like a working demo application
- [ ] Testability - [x] Testability
- [ ] snapshot ability to safe current screen (from `Renderer`) to test against - [x] snapshot ability to safe current screen (from `Renderer`) to test against
- [ ] try to integrate them into the library itself such that they also serve as examples on how to test - See [Testing](Testing.md) for details on how to create the `.zon` files containing the expected screen data
- [x] try to integrate them into the library itself such that they also serve as examples on how to test
- [x] `Layout`, `Border` and `Rectangle` contain the test cases for rendering of `Container`s
- [x] `Scrollable` test cases containing inclusion of test creation containing user interaction (i.e. mouse scrolling)

@@ -1,10 +1,10 @@
## [TODO] Testing ## Testing
Using a different testing renderer (in a corresponding namespace `zterm.testing`) renderes without flushing to the screen, but keeps the corresponding `zterm.Cell` slice for comparison. The main question would rather be how to provide the expected value for the `zterm.Cell` slice. Using a different testing renderer (in a corresponding namespace `zterm.testing`) renderes without flushing to the screen, but keeps the corresponding `zterm.Cell` slice for comparison. The main question would rather be how to provide the expected value for the `zterm.Cell` slice.
The test renderer shall be configured with a given size (as the corresponding terminal for the screen size will not be available). This serves two aspects: Firstly, `.resize` Event triggering and secondly, the rendering into a `zterm.Cell` slice with the provided size dimensions (for testing layout handling and rendering (i.e. of a scrollable element, etc.)). The test renderer shall be configured with a given size (as the corresponding terminal for the screen size will not be available). This serves two aspects: Firstly, `.resize` Event triggering and secondly, the rendering into a `zterm.Cell` slice with the provided size dimensions (for testing layout handling and rendering (i.e. of a scrollable element, etc.)).
For testing `Container`s and/or `Element`s content generation the correspondingly rendered `Cell` slices are tested. For this the test creation is two steped. The first step is the creation of the `.zon` file which contains the expected `Cell` slice for the screen you want to test (you can also test multiple screens - which would be necessary when testing for interactivity). Secondly you create the test case now against the created `.zon` file. For testing `Container`s and/or `Element`s content generation the correspondingly rendered `Cell` slices are tested. For this the test creation is two stepped. The first step is the creation of the `.zon` file which contains the expected `Cell` slice for the screen you want to test (you can also test multiple screens - which would be necessary when testing for interactivity). Secondly you create the test case now against the created `.zon` file.
## Zon Test file creation ## Zon Test file creation
@@ -26,16 +26,17 @@ test "create container zon file" {
.cols = 30, .cols = 30,
}; };
// NOTE: this is dependent on the working directory the test will be executed in
const file = try std.fs.cwd().createFile("src/test/<path>.zon", .{ .truncate = true });
defer file.close();
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
var renderer: testing.Renderer = .init(allocator, size); var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit(); defer renderer.deinit();
try container.handle(.{ .resize = size }); try container.handle(.{ .resize = size });
try renderer.render(Container(event.SystemEvent), &container); try renderer.render(Container(event.SystemEvent), &container);
// NOTE: this is dependent on the working directory the test will be executed in
const file = try std.fs.cwd().createFile("src/test/<path>.zon", .{ .truncate = true });
defer file.close();
// write the rendered `Cell` slice to disk
try renderer.save(file.writer()); try renderer.save(file.writer());
} }
``` ```
@@ -63,3 +64,103 @@ test "test container against zon file" {
}, &container, @import("test/<path>.zon")); }, &container, @import("test/<path>.zon"));
} }
``` ```
## Testing user interactivity
When testing with further user interaction the `zterm.testing.expectContainerScreen` function cannot be used. The function creates a *renderer*, *initializes* the provided container and renderes the *screen* for comparison.
This is the implementation of `zterm.testing.expectContainerScreen`:
```zig
pub fn expectContainerScreen(size: Size, container: *Container(event.SystemEvent), expected: []const Cell) !void {
const allocator = std.testing.allocator;
var renderer: Renderer = .init(allocator, size);
defer renderer.deinit();
try container.handle(.{ .resize = size });
try renderer.render(Container(event.SystemEvent), container);
try expectEqualCells(renderer.size, expected, renderer.screen);
}
```
> [!TIP]
> Create serveral *checkpoint screens* that should be tested against when testing for user interaction.
For testing user interaction you need to provide the container with the corresponding *events* it should receive. This includes user key presses, mouse presses (with their corresponding location on the screen), etc. For example see the test for `Scrollable` elements, which create the scrollable `Container` renders it and tests the screen, then scrolls down, re-renders the `Container` to test and finally tries to scroll further down to test for scrolling down after reaching the end of the `Scrollable` element:
```zig
test "scrollable vertical" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const size: Size = .{
.rows = 20,
.cols = 30,
};
var box: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.sides = .all,
.color = .red,
},
.layout = .{
.separator = .{
.enabled = true,
.color = .red,
},
.direction = .vertical,
.padding = .all(1),
},
}, .{});
try box.append(try .init(std.testing.allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
try box.append(try .init(std.testing.allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .init(box, .{ .rows = size.rows + 15 });
var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
.border = .{
.color = .green,
.sides = .vertical,
},
}, scrollable.element());
defer container.deinit();
const allocator = std.testing.allocator;
var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit();
try container.handle(.{ .resize = size });
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen);
// scroll down 15 times (exactly to the end)
for (0..15) |_| try container.handle(.{
.mouse = .{
.button = .wheel_down,
.kind = .press,
.col = 5,
.row = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// further scrolling down will not change anything
try container.handle(.{
.mouse = .{
.button = .wheel_down,
.kind = .press,
.col = 5,
.row = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
}
```