Skip to content
This repository has been archived by the owner on Oct 13, 2021. It is now read-only.

Proposal: Theming #74

Closed
cceckman opened this issue Nov 25, 2017 · 4 comments
Closed

Proposal: Theming #74

cceckman opened this issue Nov 25, 2017 · 4 comments
Labels

Comments

@cceckman
Copy link
Contributor

Splitting off of #60, also trying to address #45 :

Styling / theming support in tui is pretty limited at the moment. There's lots of elements where it would be useful to apply custom coloring / styles to. But as is usually the case, API design is hard: getting the right balance of knobs and low-friction.


Brainstorming (copied from here):

Terms:

  • Theme: a collection of Styles
  • Style: a collection of drawing characteristics: FG/BG color, underline/bold, reverse, etc.
  • Style policy: A mechanism by which a Widget’s Style is selected or determined.

Requirements:

  • Dynamic theming: ui.SetTheme works dynamically, repeatedly, etc.
  • Separate stylesheets: It should be possible to specify a theme outside of the program flow; changing a theme should not require parsing the Widget tree. This implies some notion of Theme-as-a-directory / Theme-as-a-lookup-table. But also makes cceckman’s request for static correctness difficult.
  • Dynamic styling: The Style of a Widget will change over its lifetime, e.g. on selection / deselection events. (Note, this is in addition to "dynamic theming" above.)
  • Cascading Style: cascading styles should be supported and work properly; a Widget should be able to inherit some style properties from its parent. As a motivating e

Wants:

  • [cceckman] Specifying a theme shouldn’t require duplicating the Widget hierarchy.
    I don't like the proposal in CSS-like selectors for theming #36 - making a CSS-like DSL for associating Widgets with styles after the fact. The programmer already creates the Widget hierarchy when instantiating the view. Why not require style specification at that time? (Answer: because “style” and “style policy” are coupled; see below.)
  • [cceckman] static correctness: I would like compilation to ensure that the Styles my Widgets want will be present when I run the program. But I suspect this would put too much friction into the programming process.
    • A weaker variant: SetTheme, SetStyle should return an error if the requested style or style policy is unavailable. Right now Draw just transparently ignores.

Proposal: StylePolicy

  • Separate “style” and “style policy”. The Style in use changes over the lifetime of a widget, e.g. on select / deselect; but the StylePolicy shouldn’t. So: apply a StylePolicy at Widget initialization time, rather than a Style, and let the Style be selected at draw time.

    • A style policy could say “use the parent normally, but reverse it if I’m selected”
    • A style policy could say “use these specific colors”
  • A style policy could say “use the style with this name in my Theme” - which may change over time, e.g. on a SetTheme.

  • What does that look like, mechanically?

    • type StylePolicy func(t Theme, parent Style) Style
    • If the StylePolicy is a closure on the Widget, it can do useful things like predicating on properties (e.g. IsSelected)
    • Theme parameter allows it to be a lookup.
      • Should Theme contain both StylePolicies and Styles?
    • Parent parameter allows it to cascade.
    • WidgetBase gets SetStylePolicy(s StylePolicy)
func (w *WidgetBase) Draw(p Painter) {
	p.PushStyle(w.StylePolicy(p.Style()))
	w.drawInternal(p)
	p.PopStyle()
}

(New comments from here.)

Drawbacks of StylePolicy

  1. Doesn't on its own handle Widgets with multiple styles.
  • Some of these are OK, e.g. focus/unfocus; they're based on the Widget's state.
  • But some of these don't work with StylePolicy because they're styling sub-widget items, and based on sub-widget or draw state.
  • Ex: Box has separate border and non-border styles.
  • Ex: List applies styles to its items, and predicates them on whether the item is selected / deselected.
  • Would need separate StylePolicy methods for each of these styled elements.
  1. Function-as-a-property isn't embedding.
  • Even without special style support, I can apply styles to arbitrary widgets:
type topic struct {
	*tui.Label
}

func (t *topic) Draw(p *tui.Painter) {
	p.WithStyle("reverse", t.Label.Draw)
}
  • Is embedding of widgets, and overriding methods, preferable to creating StylePolicy functions?

Proposal: Embedding

To address (1) above: Each class of Widget has a fixed number of styles it may apply.

  • Box has border as well.
  • List has selected / unselected.

These are the styles used in its Draw method; Draw effectively calls p.WithStyle(style, drawElement).

Different instances of that widget may want to have different styles. e.g.: #45 / #46 request different Boxes to have different styles.

Rather than a separate StylePolicy type, have individual methods which return style selectors:

type Widget interface {
  // ... existing methods
  Style() string
}

and, in some cases, multiple methods:

type (b *Box) BorderStyle() string {
  return b.Style() + '.border'
}

which Draw calls as appropriate. A non-default style can be applied by embedding the tui widget in another struct, overriding the Style methods. (I think I've got Go's dispatch right in my head, that that would work?)

The (default) behavior of returning "" could be used to indicate "no special style", i.e. inherit the parent style.

Proposal: Cascading styles

One coda: I think it would make code easier to reason about if the default Style was defined as "inherit the parent style"; only by explicitly setting a field would it be reconfigured. This would allow a style specification to say "the same as before, only underlined" without explicitly reading the currently-active style. (I don't think this is the case today the boolean fields: Reverse, Bold, Underline.)

@taylorchu
Copy link
Contributor

taylorchu commented Nov 26, 2017

👍 Like your thoughts

I also wonder what approach tui will take.

There are some ideas for theming: bottom-up, or fully-custom.

I hope tui takes the bottom-up approach likes html, where a set of base elements is defined. Each base element has stylable defined. The users, who dont care about how exactly to draw yellow bold text or red border, will composite those base elements. In this approach, theming is a lot easier if the composite tree is traversable explicitly (we are not there yet?). A style applier can then apply style to any widget, before passing it to tui.UI.

I don't have a concrete thought on dsl yet, but the idea is close to css.

@marcusolsson
Copy link
Owner

@cceckman First off, awesome work with the proposal! I'm intrigued by the idea of the StylePolicy but I think I at least would need to see some more examples from the perspective of a user. Meanwhile, I definitely think style inheritance is the solution to #60 at least. I think we can do some work here while discussing the StylePolicy idea.

@taylorchu The HTML/CSS approach was my initial idea, due to it being a tried and tested approach to separating structure from presentation. I'd like to see other approaches to this problem though. And no, the widget tree is not traversable yet but I think it might be time to implement it for theming purposes. Also, it would be useful for tabbing through widgets (which is a bit basic right now).

@cceckman
Copy link
Contributor Author

Yeah, I'd originally brainstormed towards StylePolicy, but it does have the issue of tying the widget tree very closely with presentation. I don't like it much any more.

I've got a few things to say against getting approaching CSS as a model:

  • CSS is responsible for color, font, size, several kinds of positioning, border shape and style... a lot of knobs that need to be mixed and matched. In a tui application, Style is only responsible for about 5 parameters (Fg/Bg, B/I/U); borders and sizing are handled separately, with method calls. CSS has more demand for mixing-and-matching pieces of policy than tui, which motivates a more complicated selection scheme.

  • I'd bet tui applications have fewer "styled objects". A quick measure shows my laptop screen will show me 166x57 characters, while CSS has to account for close to all of the 1500x1000 pixels. (Yeah, it's a weird resolution, blame the DPI.) A web page, or normal-UI application, has a lot more space to work with, and can represent a lot more different widgets.

  • Golang already has two languages - the language itself, and the templating language. The semantics of the latter are nontrivial, and aren't the same as the core Go language. I don't think we should introduce any new system that require a parser to use (and, yes, I'm counting strings.Split as a parser)- I don't think the cognitive overhead is warranted.

I definitely think there's lessons to be learned from it- in particular, I'm a fan of having Theme (a set of named Styles) separate from the Widget --> name configuration. But I still don't think a larger DSL - larger than "key lookup" - is justified / helpful for tui.


I like the idea of embedding as a way to define a base style + derivative styles for a Widget... except the thing I'm thinking of isn't embedding, actually, it's subclassing. A call to a GetStyle() string etc. method of a widget, from within its Draw, gets static dispatch to the embedded object, not to the parent with an overridden behavior.

Here's a Gist to that effect. I'm not sure how to get the dynamic-dispatch behavior in Go without making things significantly harder.


New proposal: StyleCollection

Make the zero value of every field in Style have the semantics of "inherit from the parent".

Add a new type:

type StyleCollection struct {
  Base Style
  Selected Style // Inherits from Base
  Border Style // Inherits from Base
}

Add to WidgetBase:

type WidgetBase struct {
  styleCollection string
  // ...
}

func (w *WidgetBase) SetStyleCollection(name string) {
  w.styleCollection = name
}
// Implements Widget.StyleCollection
func (w *WidgetBase) StyleCollection() string { return w.styleCollection }

Refactor Theme as a map[string]StyleCollection.

Have Draw of each method select the StyleCollection out of the theme, and wrap the appropriate portion of drawing with the relevant Style.

Comments

  • Easy to make sensible defaults
  • Easy to tweak: in the same way that SizePolicy can be set with a method call on widget setup, a user that wants a custom Style on a Widget can set that up at instantiation time.
  • No DSL: The collection name is an opaque string, no parsing / splitting / etc.

Perhaps we need a separate issue for that first bit, "Style has a zero value of 'inherit'"? Seems like we're pretty well agreed on that. #78 gets a start at it but I think the Style type itself needs some tweaks to have that work (e.g. making the B/U fields tri-state rather than boolean.)

@marcusolsson
Copy link
Owner

Considering this done by now

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

3 participants