~eliasnaur/gio#33:
Proposal: add layout.Context

Layout function signatures are quite unwieldy:

func (e *Editor) Layout(cfg ui.Config, queue input.Queue, ops *ui.Ops, cs layout.Constraints) layout.Dimensions

func (l *List) Init(cfg ui.Config, q input.Queue, ops *ui.Ops, cs Constraints, len int)

I propose a layout.Context type for aggregating the common arguments, similar to the Context type from Gregory Pomerantz' giowrap experiment. The simplest implementation is the straightforward struct:

type Context struct {
    Config ui.Config
    Ops    *ui.Ops
    Queue  input.Queue
}

Reducing the above signatures to:

func (e *Editor) Layout(c layout.Context, cs layout.Constraints) layout.Dimensions
func (l *List) Init(c layout.Context, cs Constraints, len int)

I decided against including the constraints to Context because constraints change all the time during layout, while the other values almost never change.

Status
REPORTED
Submitter
~eliasnaur
Assigned to
No-one
Submitted
20 days ago
Updated
16 days ago
Labels
No labels applied.

~dennwc 20 days ago

Sounds good, I see no harm in simplifying things. The library clearly states that API may break, so it should be OK :)

~theclapp 20 days ago

On Fri, Aug 30, 2019 at 9:42 AM ~eliasnaur outgoing@sr.ht wrote:

I propose a layout.Context type for aggregating the common arguments, similar to the Context type from Gregory Pomerantz' giowrap experiment. The simplest implementation is the straightforward struct:

type Context struct {
    Config ui.Config
    Ops    *ui.Ops
    Queue  input.Queue
}

Reducing the above signatures to:

func (e *Editor) Layout(c layout.Context, cs layout.Constraints) layout.Dimensions
func (l *List) Init(c layout.Context, cs Constraints, len int)

I decided against including the constraints to Context because constraints change all the time during layout, while the other values almost never change.

I'm in favor.

Is there any reason to not just store either the Context, or its component members, right in a widget (Editor, List, etc)? You said yourself they almost never change. Ops is already a pointer, and ui.Config and input.Queue are both interfaces, both of which are typically (solely?) implemented by methods on pointers, so you could change the thing pointed to and everyone using it would see the updates.

(This could be a horrible idea. Just asking.)

As for this type itself, seems like it could be confused with a context.Context? Maybe call it a GContext (G for Gio, natch, or gui, or graphics) or WContext (W for Window). Again, just a thought. Arguably the layout. prefix should be enough.

In any case, if this is adopted, please stay away from calling variables of type layout.Context ctx, since I think that definitely would be confused with context.Context! Maybe gctx there.

-- L

~eliasnaur 20 days ago

On Fri Aug 30, 2019 at 3:17 PM ~theclapp wrote:

Is there any reason to not just store either the Context, or its component members, right in a widget (Editor, List, etc)? You said yourself they almost never change. Ops is already a pointer, and ui.Config and input.Queue are both interfaces, both of which are typically (solely?) implemented by methods on pointers, so you could change the thing pointed to and everyone using it would see the updates.

Good question. Off the top of my head:

  • The zero value of Context is not useful, so zero values of widgets with embedded Contexts will not be useful either.
  • When you want to change something, you often don't want the change for everyone. For example, disabling input in a section of you UI is as simple as changing the Queue of the Context you pass into the section layout function.
  • Transient objects such as text.Label, layout.Inset are shorter with Context arguments. For standalone layout functions you can't avoid a parameter.

Gregory Pomerantz 20 days ago

The motivation for my Context approach was to present a uniform API which has the benefits of being both easier to learn and facilitating composition. All widgets have a Layout function that takes a pointer to a Context and nothing more. i.e.:

type Widget interface {     Layout(*Context) }

This requires the context to also carry along constraints and dimensions, alleviating the need for the user to remember which widgets need which arguments. The Gio user (or a container object itself) does not need to know what kind of widgets will be contained, only that they have layout functions that accept (and potentially modify) a Context.

Now this doesn't always make things easier, e.g. where a container does not care about how big its children are and will want to disregard any changes to constraints and dimensions made by child Layout functions. So there are some pros and cons to this approach that need to be thought through.

Wrapping things up in the context obscures the plumbing aspects of the UI, which I think makes things easier to understand when glancing at a section of code, you can concentrate on what UI elements are being opened, closed and laid out, without having to remember how all the plumbing is getting hooked up behind the scenes. This should not result in excess copying or memory allocation, as ctx is a pointer and widgets are only accessing the struct entries that they need.

Keeping constraints and dimensions in Context will save you from making some errors that I think will become common, for example forgetting to assign dims to the result of a Layout function and calling End() on a list or flex with the wrong dimensions. This will get past the compiler because disregarding a return value is not a compiler error or warning. Bugs like this will result in odd layout results and may be tricky to hunt down, especially for beginners.

UI code might look like this:

ctx.Reset(&e) // ctx keeps a pointer to e.Config, so we can do... ctx.RigidConstraints() {         f1 := layout.Flex{Axis: layout.Vertical} // maybe pass ctx in here?         ins := layout.UniformInset(ui.Dp(10))

        ins.Begin(ctx) // containers like Insets and Flex keep a pointer to ctx         f1.Init(ctx)         f1.Rigid()         editor1.Layout(ctx)         f1.End()         {                 f1.Flexible(0.33)                 ins := layout.UniformInset(ui.Dp(10))                 ins.Begin(ctx)                 editor2.Layout(ctx)                 ins.End()         }         f1.End()         {                 f1.Flexible(0.67)                 f2 := layout.Flex{Axis: layout.Horizontal}                 f2.Init(ctx)                 f2.Rigid()                 button1.Layout(ctx)                 c1 := f2.End()                 f2.Flexible(0.5)                 button2.Layout(ctx)                 c2 := f2.End()

                f2.Layout(c1, c2)         }         c3 := f1.End()         f1.Layout(c1, c2, c3)         ins.End() }

Gregory Pomerantz 20 days ago

ok so comments by email do not preserve code formatting. See here: https://git.wow.st/gmp/giowrap/src/master/examples/ctx.go

~eliasnaur 20 days ago

On Fri Aug 30, 2019 at 6:51 PM Gregory Pomerantz wrote:

ok so comments by email do not preserve code formatting.

I believe you can preserve formatting if you indent it:

ctx.Reset(&e) // ctx keeps a pointer to e.Config, so we can do...
ctx.RigidConstraints()
{
    f1 := layout.Flex{Axis: layout.Vertical} // maybe pass ctx in here?
    ins := layout.UniformInset(ui.Dp(10))
    ins.Begin(ctx) // containers like Insets and Flex keep a pointer to ctx
    f1.Init(ctx)
    f1.Rigid()
    editor1.Layout(ctx)
    f1.End()
    {
...

~theclapp 16 days ago

So once we get multiple top-level windows (https://todo.sr.ht/~eliasnaur/gio/19), would a Context struct have to have a reference to the window it's in, or is everything any widget needs to know in Config, Ops, and Queue already?

Gregory Pomerantz 16 days ago

My version does store a reference to the window within Context, but that is not necessary. You would need to update the window using the right context, but this could be done manually, or by storing a reference to a Context within the window struct itself, so that you can just call Window.Update() with no parameters and it will use the context (and its embedded ops) it already has. With this approach, the Window update event could even provide you with a fresh context that is ready to use on each frame, and save you from having to manually call ops.Reset(), ask for the window Config and set constraints based on the current window size. You would just assign ctx := e.Context or something like that prior to your layout calls.

~eliasnaur 16 days ago

On Tue Sep 3, 2019 at 2:30 PM ~theclapp wrote:

So once we get multiple top-level windows (https://todo.sr.ht/~eliasnaur/gio/19), would a Context struct have to have a reference to the window it's in, or is everything any widget needs to know in Config, Ops, and Queue already?

My intention is that everything in package app, including Window, should only be a concern to the top-level part of a Gio program. The primary reason being that package app contains all the non-Go parts that might not be available in a headless test or a CI environment.

-- elias

Register here or Log in to comment, or comment via email.