oyvindberg / tui-scala

Beautiful Text-based User Interfaces for Scala
MIT License
207 stars 11 forks source link

Make `Layout` a widget #41

Open oyvindberg opened 1 year ago

oyvindberg commented 1 year ago

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)
          )
        )
    )
  }
dejvid commented 1 month ago

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.