cursive
cursive copied to clipboard
Load layout from configuration file
It may be easier to define some complex layouts in external files rather than build it manually in the source code.
Would need to define a layout representation (xml? json? yaml? qml?)... We have a toml parser already to read themes, but toml doesn't seem like a good fit for this.
Using a language with an existing layout connotation (like qml or html) may look weird if we have to limit/change the actually usable elements.
IMHO, XML has some nice ways of describing meta-information you may have for a view, so I guess it would be a good fit. I consider XML ugly, though I have to admit that item attributes are a nice thing that JSON or YAML do not have.
I like TOML, though I'd agree taht it is not that appropriate for the problem.
I've been eyeing yaml lately, as a more readable JSON. Here is an example file describing a basic application:
definitions:
- first:
Dialog:
child:
TextView:
text: >
This is a long text!
A little too long to
fit on one line.
buttons:
- label: Ok
callback: $next
- label: Ok
callback: $quit
with:
- fixed_width: 8
- next:
Dialog:
title: Congratulations!
child:
TextView:
text: Thank you for your time.
buttons:
- label: Quit
callback: $quit
layers:
- $first
And the corresponding JSON:
{
"definitions": [
{
"first": {
"Dialog": {
"buttons": [
{
"callback": "$next",
"label": "Ok"
},
{
"callback": "$quit",
"label": "Ok"
}
],
"child": {
"TextView": {
"text": "This is a long text! A little too long to fit on one line.\n"
}
},
"with": [
{
"fixed_width": 8
}
]
}
}
},
{
"next": {
"Dialog": {
"buttons": [
{
"callback": "$quit",
"label": "Quit"
}
],
"child": {
"TextView": {
"text": "Thank you for your time."
}
},
"title": "Congratulations!"
}
}
}
],
"layers": [
"$first"
]
}
The parser would take a map of callbacks, and the config file would reference those (callback: $quit
for instance). Not sure yet how to handle different kinds of callbacks (like TextView::on_submit
).
Here is the same example, written in a possible xml format:
<definitions>
<Dialog name="first">
<child>
<TextView>
<text>This is a long text! A little too long to fit on one line.</text>
</TextView>
</child>
<buttons>
<button callback="$next">Ok</button>
<button callback="$quit">Cancel</button>
</buttons>
<with>
<fixed_width>8</fixed_width>
</with>
</Dialog>
<Dialog name="next">
<title>Congratulations!</title>
<child>
<TextView>
<text>Thank your for your time.</text>
</TextView>
</child>
<buttons>
<button callback="$quit">Quit</button>
</buttons>
</Dialog>
</definitions>
<layers>
<layer>$first</layer>
</layers>
Hm. I do not like any of these, I'm sorry. I would rather go for a full templating language,... something like "erb" for Ruby. I'm not sure whether we have something like this in rust, yet, but I kind of remember reading something about templating languages.
Templating languages are usually meant to modify a legacy language (mostly HTML) which doesn't support variables, by "reverse-embedding" a separate language (It looks like an external language in embedded in HTML, though what really happens is the HTML itself ends up embedded and parsed). I'm not sure it's ideal when we can control the initial format to begin with.
Also, there should probably be no logic in those files, it's really just a layout description. Referencing "outside variables" (like the callbacks in the example, or possibly other values) should be included, but I don't think this would require a full turing-complete language.
On the other hand, the declarations
and layers
parts may also be a bad idea; I'm now thinking of having a layout file per "component" that could be instantiated from the code, like in the Android UI framework. Or it could all be in the same file, for instance using yaml's document separator. That's probably bikeshedding at this point.
About the callbacks, I made a simple experiment using Any
, and it seems to work: https://is.gd/mqBk4W.
Views would have to implement something like fn parse_config(ConfigBlock, ParsingContext) -> Self
.
Desired types would be registered in the context, along with a keyword (probably the type's name).
The whole process would look like this:
- The layout file is parsed into a configuration object
- A context is initialized by Cursive with the standard views pre-registered
- The developper register his own custom views
- The developper "instantiates" a view from the config object + context
- The config object finds the "root" required by the user, look at the identifier, and calls the corresponding registered view's
parse_config
.- This method, in turn, can ask the config object to instantiate some views based on a child node.
- It can also ask the context for some specific values, like a
String
, aBox<Fn(&mut Cursive)>
, etc...
- The config object finds the "root" required by the user, look at the identifier, and calls the corresponding registered view's
- In a callback method, user input is pushed into the context, and more views are instantiated.
Example usage:
extern crate cursive;
fn main() {
let mut siv = Cursive::new();
// Cursive keeps an internal config object and context.
// This fills the config object with the data from the given file.
siv.parse_file("./layouts/first").unwrap();
// This fills the context with some data: here, a callback function
parser.register_callback("$next", |s| {
s.pop_layer();
s.register_data("$answer", "This might have been user input!");
// We can instantiate views from callbacks too
s.add_layer(s.instantiate("next").unwrap());
});
parser.register_callback("$quit", |s| s.quit());
siv.add_layer(siv.instantiate("first").unwrap());
siv.run();
}
Hmm, the most painful point with the layout format right now is lists of views (select view, linearlayout, ...) with variable size. It might be easier to create those from the code, potentially instantiating those from another model. Or we could find some directives to iterate on simple lists, provided that the child view know how to handle it. It does show one situation where the same model is instantiated multiple times with different context. Have to see if mutating a global context between each instantiation is a good idea, or if spawning a "sub-context" per view is a better solution.
Just leaving some notes for later: I initially feared registering the view names would have to be done manually with a big list of all the supported views, but inventory or linkme (both awesome crates from dtolnay) look like great ways to maintain a decentralized registry - we may be able to write a proc-macro crate to bring a #[cursive::FromConfig]
or something which would help make a view parsable from a config blob.
Let me just update my statement on config file format: Please don't do XML and PLEASE, PLEASE don't do YAML. YAML is pure hell! Use the Rust standard: TOML. It is good, it is understood, it is supported. I'd rather go XML than YAML, TBH.
I think at first I'll focus on working with a config blob, not necessarily with a specific serialization format. TOML is pretty bad for nested data, but ideally it'll still be possible to implement a parser for that or any other format from outside of this crate.
Maybe just a proc macro would be the easiest and most rusty solution...