Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Layout a widget #41

Open
oyvindberg opened this issue Apr 16, 2023 · 1 comment
Open

Make Layout a widget #41

oyvindberg opened this issue Apr 16, 2023 · 1 comment

Comments

@oyvindberg
Copy link
Owner

The most absolutely terrible part of the API is the layouting. This is my current idea for making it better:

From this:

  def ui(f: Frame, app: App): Unit = {
    val verticalChunks = Layout(
      direction = Direction.Vertical,
      margin = Margin(2, 2),
      constraints = Array(Constraint.Percentage(50), Constraint.Percentage(50)),
    ).split(f.size)

    val barchart1 = BarChartWidget(
      block = Some(BlockWidget(title = Some(Spans.nostyle("Data1")), borders = Borders.ALL)),
      data = app.data,
      bar_width = 9,
      bar_style = Style(fg = Some(Color.Yellow)),
      value_style = Style(fg = Some(Color.Black), bg = Some(Color.Yellow))
    )
    f.render_widget(barchart1, verticalChunks(0))

    val horizontalChunks = Layout(
      direction = Direction.Horizontal,
      constraints = Array(Constraint.Percentage(50), Constraint.Percentage(50))
    ).split(verticalChunks(1))

    val barchart2 = BarChartWidget(
      block = Some(BlockWidget(title = Some(Spans.nostyle("Data2")), borders = Borders.ALL)),
        bar_width = 5,
        bar_gap = 3,
        bar_style = Style(fg = Some(Color.Green)),
        value_style = Style(bg = Some(Color.Green), add_modifier = Modifier.BOLD),
        data = app.data
      )
    f.render_widget(barchart2, horizontalChunks(0))

    val barchart3 = BarChartWidget(
        block = Some(BlockWidget(title = Some(Spans.nostyle("Data3")), borders = Borders.ALL)),
        data = app.data,
        bar_style = Style(fg = Some(Color.Red)),
        bar_width = 7,
        bar_gap = 0,
        value_style = Style(bg = Some(Color.Red)),
        label_style = Style(fg = Some(Color.Cyan), add_modifier = Modifier.ITALIC)
      )
    f.render_widget(barchart3, horizontalChunks(1))
  }

to something this:

  def ui(app: App): Widget = {
    Layout(direction = Direction.Vertical, margin = Margin(2, 2))(
      Constraint.Percentage(50) -> 
        BarChartWidget(
        block = Some(BlockWidget(title = Some(Spans.nostyle("Data1")), borders = Borders.ALL)),
        data = app.data,
        bar_width = 9,
        bar_style = Style(fg = Some(Color.Yellow)),
        value_style = Style(fg = Some(Color.Black), bg = Some(Color.Yellow))
      ),
      Constraint.Percentage(50) -> 
        Layout(direction = Direction.Horizontal)(
          Constraint.Percentage(50) -> BarChartWidget(
            block = Some(BlockWidget(title = Some(Spans.nostyle("Data2")), borders = Borders.ALL)),
            bar_width = 5,
            bar_gap = 3,
            bar_style = Style(fg = Some(Color.Green)),
            value_style = Style(bg = Some(Color.Green), add_modifier = Modifier.BOLD),
            data = app.data
          ),
          Constraint.Percentage(50) -> BarChartWidget(
            block = Some(BlockWidget(title = Some(Spans.nostyle("Data3")), borders = Borders.ALL)),
            data = app.data,
            bar_style = Style(fg = Some(Color.Red)),
            bar_width = 7,
            bar_gap = 0,
            value_style = Style(bg = Some(Color.Red)),
            label_style = Style(fg = Some(Color.Cyan), add_modifier = Modifier.ITALIC)
          )
        )
    )
  }
oyvindberg added a commit that referenced this issue Apr 28, 2023
Massively improved DX aside, this is the first step towards more react-like components in tui.

Next steps I'll try:
- Make `BlockWidget` take children, so we don't need to duplicate the logic in all widgets
- Make `Widgets` return a datatype akin to `ReactElement` which will call `render` for us in most cases. Probably with a fallback
oyvindberg added a commit that referenced this issue Apr 30, 2023
Massively improved DX aside, this is the first step towards more react-like components in tui.

Next steps I'll try:
- Make `BlockWidget` take children, so we don't need to duplicate the logic in all widgets
- Make `Widgets` return a datatype akin to `ReactElement` which will call `render` for us in most cases. Probably with a fallback
@dejvid
Copy link

dejvid commented Sep 27, 2024

This is just my opinion. I am open to talking about and commenting on your statement.

Your alternative looks much better than the current one—more like a "swift ui" style. It is also more intuitive. You prepare the data structure and then render it later. You can inspect the structure for later "rendering" optimisations, constraint resolving, etc. A declarative layout can allow you to do a lot of preprocessing, and you can put them as "children" in other layouts without problems. So, rendering should be hidden from the "programmer". I think if you design a new "layout" system, that will fix the current issue where you need to call multiple render calls, and that is bad, and it doesnt scale f.render_widget(barchart2, horizontalChunks(0)).

For example, if we want to have an option for layouts to render more stuff than is available space on the screen, etc.

But I would like constraints solved by LAyouts like HLyout and VLayout and the way Swift UI solves them.
If you want to divide the screen into two parts, you have to tell the VLayout to take the fill screen space(fill or wrap to take the Vertical height from the HLayout components.

This is my pseudo code:
So VLayout wraps children or fills the available space. If it wraps, the Height of the VLayout is determined by the sum of the height of the HLayouts.
Because this layout fills the screen and the BarCharts all have equal weights, you would get 2 BarCharts each on half of the screen. You could change the weights on the layout to fill less or more screen space, etc.

Window(title :="Bar Chart", bottomText := " Press q to quit"){
  VLayout(space:=fillScreen){
    HLayout(width=fill, height=fill){
      BarChart1(..)
    },
    HLayout(width=fill, height=fill){
       BarChart2(...)
    }
  }
}

We can also add, for example, a scrollable layout where we could use more space in the layout than is available on the screen. This happens, for example, when a text file renders larger than the screen.

So, the layout would tell the constraints how large it would be. Then, you can just put children on layouts, and they will wrap or fill the available space. This would be better than introducing the Constraint.Percentage(50) -> XYZ because constraints should be on layout and elements, and it's up to the constraint resolver to see if the rendering form takes 50% of space.

It's crucial for the layouts to be 'reactive' in nature. Project Laminar effectively utilizes this technique to build HTML. In our context, this technique would be employed to construct a declarative TUI AST, which would then be rendered in accordance with the layout rules.

You need two things: the app state and a way to build TUI AST according to the App State.

So, to wrap it up, it's important to decide how the TUI AST will look and how it will mutate as the app state changes. Then, the rendering part will take the TUI AST and render it. So, it's more than just a layout system that needs to be decided but also how these layouts will react to App changes. So this reminds me of the Laminar (https://laminar.dev/)project: They have solved this activity to build AST for HTML. It is efficient. So why not use a laminar "HTML" to construct the TUI AST(we need a data structure AST that will define the layout and render elements)?

At the end of the day, the aim is to provide developers with 'Easy' building blocks for the terminal user interface. For instance, we can define form layouts that any programmer can use to enhance their CLI, just like Charm did. This system is designed with user-friendliness in mind, ensuring that it's not just functional, but also easy to use.

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

No branches or pull requests

2 participants