[GH-ISSUE #411] Dynamic layouts #299

Closed
opened 2026-03-04 01:03:44 +03:00 by kerem · 8 comments
Owner

Originally created by @gnojus on GitHub (Feb 24, 2020).
Original GitHub issue: https://github.com/rivo/tview/issues/411

I realized what I'm missing from this package. While it's great for predesigned interfaces, it's not very suitable to create dynamic layouts, for example a chat view.

I was thinking that a primitive could return minimal dimensions to fit it's contents. You could also set for example fixed width and the primitive would recalculate its required height accordingly.
Some primitive, e. g. Container could provide stacking primitives and scrolling functionality.

Or would this be outside the scope of this package?

Originally created by @gnojus on GitHub (Feb 24, 2020). Original GitHub issue: https://github.com/rivo/tview/issues/411 I realized what I'm missing from this package. While it's great for predesigned interfaces, it's not very suitable to create dynamic layouts, for example a chat view. I was thinking that a primitive could return minimal dimensions to fit it's contents. You could also set for example fixed width and the primitive would recalculate its required height accordingly. Some primitive, e. g. `Container` could provide stacking primitives and scrolling functionality. Or would this be outside the scope of this package?
kerem 2026-03-04 01:03:44 +03:00
Author
Owner

@rivo commented on GitHub (Feb 24, 2020):

Pretty much. You're talking about a layout engine as web browser implement them, where sizes propagate down as well as up. It's not just the implementation which is highly sophisticated and therefore way beyond the scope of this project. You will then also need some kind of CSS-like language to describe the layout.

In Grid and Flex, I'm already using some tricks (with negative numbers) to differentiate between proportional and absolute sizes. If you now add to this things like "min-width" and "max-width", "auto", "inherited", or even fun stuff like fractional size units (see "fr" in CSS grids), the complexity will not be manageable anymore (and we'll definitely have to break backwards-compatibility). Maybe you're then better off using Lynx, w3m or something like that.

<!-- gh-comment-id:590404139 --> @rivo commented on GitHub (Feb 24, 2020): Pretty much. You're talking about a layout engine as web browser implement them, where sizes propagate down as well as up. It's not just the implementation which is highly sophisticated and therefore way beyond the scope of this project. You will then also need some kind of CSS-like language to describe the layout. In `Grid` and `Flex`, I'm already using some tricks (with negative numbers) to differentiate between proportional and absolute sizes. If you now add to this things like "min-width" and "max-width", "auto", "inherited", or even fun stuff like fractional size units (see "fr" in CSS grids), the complexity will not be manageable anymore (and we'll definitely have to break backwards-compatibility). Maybe you're then better off using Lynx, w3m or something like that.
Author
Owner

@tslocum commented on GitHub (Feb 24, 2020):

I agree with rivo, but I'm open to adding this functionality to cview if you or anyone else is interested in working on an initial implementation (I'm glad to help).

<!-- gh-comment-id:590411055 --> @tslocum commented on GitHub (Feb 24, 2020): I agree with rivo, but I'm open to adding this functionality to [cview](https://gitlab.com/tslocum/cview) if you or anyone else is interested in working on an initial implementation (I'm glad to help).
Author
Owner

@gnojus commented on GitHub (Feb 24, 2020):

@rivo ok, I understand.
@tslocum Yes I'm interested in such implementation, and I have been thinking about it lately.

<!-- gh-comment-id:590415084 --> @gnojus commented on GitHub (Feb 24, 2020): @rivo ok, I understand. @tslocum Yes I'm interested in such implementation, and I have been thinking about it lately.
Author
Owner

@gnojus commented on GitHub (Feb 29, 2020):

I decided to try and implement this and came up with a simple, but yet very useful solution (at least from my perspective). I only made the basic implementation, because I thought that rivo might actually see this as an useful addition to tview.

The idea:

  • Adding two functions to Primitive: GetWidth(height) and GetHeight(width). These calculate and return the minimum required size accordingly to the given opposite measurement so that it can fully fit all its contents inside such box.
    Maybe this should be an interface that only some primitives implement, I'm not sure:
    type DynamicPrimitive interface {
        Primitive
        GetWidth(height int) int
        GetHeight(width int) int
    }
    
  • Grid and Flex would calculate required width/height by summing all the required sizes by items inside.
  • New primitive Container. Acts simmillary to a Flex, has row/column direction and allows to append items with either fixed size or dynamic if fixedSize < 0. Container would respect size required by primitives (via GetWidth/GetHeight) and wouldn't care about fitting items inside - it would just make everything scrollable.

Implementation:
You can find basic implementation in https://github.com/Nojus297/tview (branch dynamic). Its pretty much a copy-paste from Flex. The only primitives that have GetWidth/GetHeight actually implemented are TextView and Flex (Box only keeps the default 15:10 ratio) and there is an example using these in demos/container folder.

I hope you (@rivo) can find some time to look at this and say whether this is a viable addition to tview. If so, I would gladly help to make this. Anyway, let me know you thoughts.

<!-- gh-comment-id:592992026 --> @gnojus commented on GitHub (Feb 29, 2020): I decided to try and implement this and came up with a simple, but yet very useful solution (at least from my perspective). I only made the basic implementation, because I thought that rivo might actually see this as an useful addition to `tview`. __The idea:__ - Adding two functions to `Primitive`: `GetWidth(height)` and `GetHeight(width)`. These calculate and return the minimum required size accordingly to the given opposite measurement so that it can fully fit all its contents inside such box. Maybe this should be an interface that only some primitives implement, I'm not sure: ```go type DynamicPrimitive interface { Primitive GetWidth(height int) int GetHeight(width int) int } ``` - `Grid` and `Flex` would calculate required width/height by summing all the required sizes by items inside. - New primitive `Container`. Acts simmillary to a `Flex`, has row/column direction and allows to append items with either fixed size or dynamic if `fixedSize` < 0. `Container` would respect size required by primitives (via `GetWidth`/`GetHeight`) and wouldn't care about fitting items inside - it would just make everything scrollable. __Implementation:__ You can find basic implementation in https://github.com/Nojus297/tview (branch `dynamic`). Its pretty much a copy-paste from `Flex`. The only primitives that have `GetWidth`/`GetHeight` actually implemented are `TextView` and `Flex` (`Box` only keeps the default 15:10 ratio) and there is an example using these in `demos/container` folder. I hope you (@rivo) can find some time to look at this and say whether this is a viable addition to tview. If so, I would gladly help to make this. Anyway, let me know you thoughts.
Author
Owner

@rivo commented on GitHub (Apr 14, 2020):

So from glancing over your code, your Container class is Flex but without proportional sizes. You replace them with sizes calculated by the primitives themselves. How are those sizes calculated? Well, in 2D space we have two sizes, width and height. Your suggestion is that given one, primitives can calculate an optimal value for the other one.

But the only case where this is actually useful is when calculating the height for a TextView given a fixed width. Its GetWidth() function makes much less sense. It even ignores its height parameter. No mention on what other primitives are supposed to do (Box tries to maintain an arbitrary 10:15 ratio. I'm not sure where this choice comes from.)

All of this seems to come down to your need to implement a chat view. (Please correct me if I'm wrong.) Isn't a TextView.GetCurrentHeight() function all you need? You can then use this to update your Flex/Grid.

<!-- gh-comment-id:613417790 --> @rivo commented on GitHub (Apr 14, 2020): So from glancing over your code, your `Container` class is `Flex` but without proportional sizes. You replace them with sizes calculated by the primitives themselves. How are those sizes calculated? Well, in 2D space we have two sizes, width and height. Your suggestion is that given one, primitives can calculate an optimal value for the other one. But the only case where this is actually useful is when calculating the height for a `TextView` given a fixed width. Its `GetWidth()` function makes much less sense. It even ignores its `height` parameter. No mention on what other primitives are supposed to do (`Box` tries to maintain an arbitrary 10:15 ratio. I'm not sure where this choice comes from.) All of this seems to come down to your need to implement a chat view. (Please correct me if I'm wrong.) Isn't a `TextView.GetCurrentHeight()` function all you need? You can then use this to update your `Flex`/`Grid`.
Author
Owner

@gnojus commented on GitHub (Apr 15, 2020):

You are right, the version that I linked previously is far from perfect and doesn't make too much sense. I have made many changes on my local branch, which is now super handy for me, but as I'm developing it I'm now less sure that it actually fits for tview.

(Box tries to maintain an arbitrary 10:15 ratio. I'm not sure where this choice comes from.)

This wasn't a good idea, but I changed it to return borders+padding. This way subclassing primitives can subtract this value when calculating their width/height from passed argument and add the opposite to the result.

func (b *Box) GetWidth(height int) int {
	width := b.paddingLeft + b.paddingRight
	if b.border {
		width += 2
	}
	return width
}

func (c *Container) GetWidth(height int) int {
    height -= c.Box.GetHeight(0) // <- Box doesn't even use the parameter

    // Calculations of all the items inside by calling their GetWidth().

    return width + c.Box.GetWidth(0)
}

The previous version shouldn't even work correctly.

No mention on what other primitives are supposed to do

Containers (Flex, Grid, Container, Pages) should sum/get maximum of their contents, depending on the parameter asked. Others might normally return 0 (like Inputfield), therefore I added minSize/maxSize parameters to Container items, which has higher priority then their wanted-calculated size.

Container class is Flex but without proportional sizes.

I did end up making percentage parameter for Container items, additional to minSize/maxSize to allow basic spacing (percentage of the free space).

All of this seems to come down to your need to implement a chat view.

This is not too far off, but such functionality could be useful for any dynamically loaded content. The key for me is that you can create custom primitives representing your data, stack them and plop everything to Container, and let it handle all the sizes and resizes. I'm also using this for a multiline list and scrollable dynamically loaded feed.

One bigger issue that came up was that everything started getting inefficient, as many TextViews had to be reindexed and all the sizes recalculated. This should be solved by some basic caching, but it will ruin the simplicity of the implementation (which isn't simple anymore though).

Anyway, I hope that it's a bit clearer now. If you wish, I could tidy up and update the Container source, or report later when I'm happy with it myself and it could maybe be useful.

<!-- gh-comment-id:613986725 --> @gnojus commented on GitHub (Apr 15, 2020): You are right, the version that I linked previously is far from perfect and doesn't make too much sense. I have made many changes on my local branch, which is now super handy for me, but as I'm developing it I'm now less sure that it actually fits for tview. > (Box tries to maintain an arbitrary 10:15 ratio. I'm not sure where this choice comes from.) This wasn't a good idea, but I changed it to return borders+padding. This way subclassing primitives can subtract this value when calculating their width/height from passed argument and add the opposite to the result. ```go func (b *Box) GetWidth(height int) int { width := b.paddingLeft + b.paddingRight if b.border { width += 2 } return width } func (c *Container) GetWidth(height int) int { height -= c.Box.GetHeight(0) // <- Box doesn't even use the parameter // Calculations of all the items inside by calling their GetWidth(). return width + c.Box.GetWidth(0) } ``` The previous version shouldn't even work correctly. > No mention on what other primitives are supposed to do Containers (`Flex`, `Grid`, `Container`, `Pages`) should sum/get maximum of their contents, depending on the parameter asked. Others might normally return 0 (like `Inputfield`), therefore I added `minSize`/`maxSize` parameters to `Container` items, which has higher priority then their wanted-calculated size. > Container class is Flex but without proportional sizes. I did end up making `percentage` parameter for `Container` items, additional to `minSize`/`maxSize` to allow basic spacing (percentage of the free space). > All of this seems to come down to your need to implement a chat view. This is not too far off, but such functionality could be useful for any dynamically loaded content. The key for me is that you can create custom primitives representing your data, stack them and plop everything to `Container`, and let it handle all the sizes and resizes. I'm also using this for a multiline list and scrollable dynamically loaded feed. One bigger issue that came up was that everything started getting inefficient, as many `TextView`s had to be reindexed and all the sizes recalculated. This should be solved by some basic caching, but it will ruin the simplicity of the implementation (which isn't simple anymore though). Anyway, I hope that it's a bit clearer now. If you wish, I could tidy up and update the `Container` source, or report later when I'm happy with it myself and it could maybe be useful.
Author
Owner

@rivo commented on GitHub (Nov 9, 2021):

I realize this would indeed add some convenience to the package. And similar issues have popped up in the past. But I feel like this would require a major overhaul of the layout functionality in tview. Together with the backwards compatibility mandate, I don't think it's an easy thing to do.

I'm leaving this open so I can come back to it at some point.

<!-- gh-comment-id:964282134 --> @rivo commented on GitHub (Nov 9, 2021): I realize this would indeed add some convenience to the package. And similar issues have popped up in the past. But I feel like this would require a major overhaul of the layout functionality in `tview`. Together with the backwards compatibility mandate, I don't think it's an easy thing to do. I'm leaving this open so I can come back to it at some point.
Author
Owner

@rivo commented on GitHub (Dec 12, 2022):

Closing this as it's not on the roadmap in the foreseeable future. We can always reopen once it becomes relevant again.

<!-- gh-comment-id:1346685307 --> @rivo commented on GitHub (Dec 12, 2022): Closing this as it's not on the roadmap in the foreseeable future. We can always reopen once it becomes relevant again.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/tview#299
No description provided.