cursive icon indicating copy to clipboard operation
cursive copied to clipboard

Load layout from configuration file

Open gyscos opened this issue 9 years ago • 9 comments

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.

gyscos avatar May 19 '15 19:05 gyscos

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.

matthiasbeyer avatar Oct 03 '16 15:10 matthiasbeyer

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>

gyscos avatar Oct 04 '16 19:10 gyscos

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.

matthiasbeyer avatar Oct 05 '16 06:10 matthiasbeyer

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, a Box<Fn(&mut Cursive)>, etc...
  • 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();
}

gyscos avatar Oct 05 '16 21:10 gyscos

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 avatar Oct 06 '16 03:10 gyscos

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.

gyscos avatar Dec 26 '20 17:12 gyscos

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.

matthiasbeyer avatar Dec 26 '20 18:12 matthiasbeyer

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.

gyscos avatar Dec 26 '20 22:12 gyscos

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

matthiasbeyer avatar Dec 26 '20 22:12 matthiasbeyer