gyscos / cursive

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

Color styling problem #253

Open njskalski opened 6 years ago

njskalski commented 6 years ago

Hi,

this is going to be a little controversial: I spend a lot of time while working with Cursive in a very non-effective way trying to get desired coloring of views. Since the styling (like applying effects, using different colors from Palette) is usually determined by the code of View::draw(...) method, I am forced to either create wrappers that modify a theme to each view in separate or fork the entire view and manually modify the drawing method. That obviously increases cost of development and maintenance, especially with complex third party views.

In views I write, I decided to move away from predefined palette, and created something like a "style tree", where I just override colors with html color codes defined in JSON. If other (default, third party etc) views used similar technique, I would be able to achieve required styling very fast, in a way a little similar to CSS.

Would that be something you could consider for Cursive? If you are open to discussion I can make a design draft. I am thinking about something like this: https://github.com/njskalski/sly-editor/blob/master/src/default_settings.rs (theme part) but more streamlined. Like "cascading, json defined themes", that could be deserialized into objects via serde_json, so one can either configure in-code or via json.

I we'd agree on some format, I could help you to update the views, and fork and update some third party views, even help with maintaining.

gyscos commented 6 years ago

Hi!

I'm thinking about the best solution for color management.

I think a lot can be solved by using a larger palette - possibly arbitrarily large.

Let's consider an example use-case: I want to use a third-party view likew TreeView while customizing the color it uses for arrows; and I want this color to be different in a special popup I'm sometimes creating.

The TreeView will specify that it uses a specific color from the Palette for arrows - it could be Secondary, or something custom like TreeViewArrows. I'll just need to update this value in my palette to change this color.

Now, to have a different color when the TreeView is in a popup, the popup will need to specify a different TreeViewArrows color. To do this, it might copy over the MyPopupContent.TreeViewArrows value - it might actually copy over any value in MyPopupContent.*, so we can also re-define the background, or other colors.

So a first simple solution would be to allow any string as extra key in the palette.

An example toml configuration would look like:

[colors]
    primary = "black"
    TreeViewArrows = "red"
    "MyPopupContent.TreeViewArrows" = "green"
    "MyPopupContent.background" = "white"

Now, a single large, flat palette of colors like this might work, but is less than ideal to use. There is no clear hierarchy between keys, apart from a common prefix. We could try to use an actual tree:

[colors]
    primary = "black"
    TreeViewArrows = "red"

    [MyPopupContent]
        TreeViewArrows = "green"
        background = "white"

However, it does make the structure a bit more complex: instead of something like a Map<String, Color>, we now have a more free-form structure. It probably doesn't matter so much; we're already parsing the toml table manually anyway.

The custom part of the Palette itself may end up looking like a tree:

enum PaletteValue {
    Leaf(Color),
    Node(HashMap<String, PaletteValue>),
}

Third-party views that need more than the current 10 values could then define the extra keys they'll use as constants, so users at least don't have to write string literals.

njskalski commented 6 years ago

So a tree like structure would be enough, but I would not encourage re-using default identifiers unless as a fallback. So for instance the arrow in TreeView should use (as example) primary text color if the lookup for "TreeViewArrows" returns empty. The reason for that is that view authors will have different ideas what "tertiary" or "background" means for them, not even talking about design taste. "One palette to fit all" can be a "prototype and hack" fallback, but not a policy for versatile TUI library (and given variety of backends that seems to be the idea behind Cursive).

As for the string literals - I would encourage to reconsider them. As long as we cache the lookup results to avoid string comparison, they are what brought DOM styling the versatility it needed to provide rich experience. As a ex game developer I don't like them either, but I notice their usefulness in development process.

As for the TOML it's OK, but I was thinking something like CSS selectors subset (like just id's and classes) are also worth considering. That would be a bonus for users in terms of learning curve and not giving birth to another standard.

There is also another idea that just emerged in my mind. If the Palette was a trait implementing "selector to color" method, having &view, and views had ids and classes for it's colors, that would be another smart thing. This way we could do dynamic styling, based on say whether the window is active, enabled, receiving input, or even based on program state (window blinking as reaction to external signal).

gyscos commented 6 years ago

From a user point-of-view, I want to recommend sticking to the basic palette when theming an application. This is a great way to ensure a visual coherence between views written by different people. This is what other systems like material design do, where they recommend using a similar palette with primary and secondary colors (among others). This means when writing views, we must keep in mind that most users will either stick to the default palette, or redefine the basic colors. Therefore, it's important to make sure that the view fits well with this basic palette. One thing I don't want to happen is third-party views using custom key in the palette for every part and not paying attention to how the view looks when using the basic palette only.

Now, I understand there are some more complex use-cases where the basic palette, even slightly larger, would not be enough. For instance:

As for the string literals - I would encourage to reconsider them. As long as we cache the lookup results to avoid string comparison, they are what brought DOM styling the versatility it needed to provide rich experience. As a ex game developer I don't like them either, but I notice their usefulness in development process.

I'm not sure we're talking about the same thing. I meant libraries that rely on specific keys in the theme would at least define a constant with this key, so users may use the constant instead of typing the literal directly, which is prone to typos and not very refactoring-friendly.

As for the TOML it's OK, but I was thinking something like CSS selectors subset (like just id's and classes) are also worth considering. That would be a bonus for users in terms of learning curve and not giving birth to another standard.

The theme itself isn't really tied to TOML, any format could represent the same thing. I don't see the point in CSS-like selectors for classes and ids. Giving a view a class (or an ID) isn't much easier than wrapping it in a theme-changing view. So if you defined the #myclass tree in the theme, you could do MyView::new().merge_theme_from("#myclass") (with a wrapper view and a trait like Boxable).

There is also another idea that just emerged in my mind. If the Palette was a trait implementing "selector to color" method, having &view, and views had ids and classes for it's colors, that would be another smart thing. This way we could do dynamic styling, based on say whether the window is active, enabled, receiving input, or even based on program state (window blinking as reaction to external signal).

View theme based on its state is already there (views have a different color when focused).

Maybe I'm missing a use-case, but it feels more like shoehorning existing CSS methods in a domain where it may not be the best fit.

njskalski commented 6 years ago

Thank you for long answer, you convinced me in all points. How do we proceed?

gyscos commented 6 years ago

The first step is to increase the size of the Palette with a tree of custom keys. This means the Palette may not stay Copy, but I don't think it's a big loss. The Palette would get a custom entry, with a simple PaletteValue structure described in a post above. We should probably add some Index<&str> implementation to make it easy to go down the tree.

Then, we should add a wrapper view that transforms the theme of the drawer when given to the child. The idea is to make it very easy to merge a sub-tree from the theme into the root node.

njskalski commented 6 years ago

Ok, how about this? #256 If it's bad, tell me what to change, if it's ok, what next? Loading? Tests?

ssokolow commented 4 years ago

I also need something like this.

I have various projects like this one which were implemented using Python and urwid and, while urwid has its advantages, it's a poster child for what happens when duck typing goes wrong. (Its expression of a type error is some horrendously cryptic message, a dozen layers deep in the stack trace, about not being able to unpack a tuple because it has the wrong number of elements)

As you can see from the screenshot on that page, I have a decent sense of design. The problem is that Cursive doesn't appear to support producing that layout without custom code hackery while, if I had more versatile theming, I could just say "Yes, I know this is a one-line TextView, but use the theming for headers and footers" and so on.

(And I'm stubborn enough about producing exactly the user experience I want that, if I have to choose between compromising on appearance and suffering urwid's lack of a type system, I'll continue to suffer with urwid.)