gyscos / cursive

A Text User Interface library for the Rust programming language
MIT License
4.29k stars 243 forks source link

Load layout from configuration file #5

Open gyscos opened 9 years ago

gyscos commented 9 years ago

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.

matthiasbeyer commented 8 years ago

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.

gyscos commented 8 years ago

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>
matthiasbeyer commented 8 years ago

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.

gyscos commented 8 years ago

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:

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();
}
gyscos commented 8 years ago

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.

gyscos commented 3 years ago

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.

matthiasbeyer commented 3 years ago

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.

gyscos commented 3 years ago

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.

matthiasbeyer commented 3 years ago

Maybe just a proc macro would be the easiest and most rusty solution...