jtdaugherty / brick

A declarative Unix terminal UI library written in Haskell
Other
1.6k stars 164 forks source link

Horizontally scrollable tabular list widget #422

Closed amano-kenji closed 1 year ago

amano-kenji commented 1 year ago

I want help with implementing spreadsheet widget. I want it to be simple but functional.

A spreadsheet may or may not have a fixed header at the top. The header cannot be hidden by vertical scrolling.

Columns can have fixed widths or be as wide as the longest column content among all available rows. Maximum width for each column can be calculated and saved in the spreadsheet state whenever a new row is added or new rows are added.

If the last column doesn't outgrow the width of a viewport, I want an option to make it fill the remaining horizontal space greedily.

Each row can have a different number of lines.

I should be able to navigate cells by automatically scrolling the viewport horizontally. Optionally, I also want to manually scroll horizontally regardless of the selected column.

Since there can be millions of rows, it'd be beneficial to draw only visible rows.

How can I implement spreadsheet widget in a simple way? A simple tabular list demo gets close, so I think spreadsheet UI doesn't have to be complex.

jtdaugherty commented 1 year ago

How can I implement spreadsheet widget in a simple way?

What counts as simple in your case? I doubt the implementation could possibly be simple, even if the UI that the user uses looks that way.

jtdaugherty commented 1 year ago

My suggestion is to start with the tabular list demo to build your spreadsheet, and check in here as you run into difficulties. I'm not really in a position to just tell you how to implement the whole thing.

amano-kenji commented 1 year ago

Simple because it's conceptually simple and doesn't have to account for all use cases.

I'm only trying to implement a simple use case.

Why do you think it cannot be simple to implement a functionally simple spreadsheet? It would be difficult to implement something like microsoft excel UI, but that's not what I'm trying to implement.

However much complex it ends up being, with lenses and brick primitives, I think it should be at least a hundred times simpler than https://github.com/andmarti1424/sc-im which is implemented in C with ncurses.

jtdaugherty commented 1 year ago

It sounds like it would be best if we don't spend time debating what "simple" means and just stick to the implementation issues. Have you attempted to start working on this? If so, have you run into specific issues that I could help with?

amano-kenji commented 1 year ago

I will start implementing it today. I just woke up.

What I'm trying to implement is basically tabular list demo with fixed-width columns or columns as wide as longest column content among all available or visible rows. When I navigate cell by cell, horizontal scrolling should be done automatically to show the chosen cells. Manual horizontal scrolling is also considered but may not necessarily be implemented if it's too complex. A column may span multiple lines if the content has line breaks.

I think it's good to assume one row spans one line anyway and draw as many rows as the number of lines in the viewport plus a few more. I just need to keep the cost of drawing from spiraling out of control. I don't have to know the absolute minimum number of rows that should be drawn in a viewport. That's my intuition. But, I will have to read the implementations of table and list to actually guage what it'll take to implement a horizontally scrollable version of tabular list.

I will get back if I have questions.

amano-kenji commented 1 year ago

It turns out that I needed a horizontally scrollable tabular list widget with cell-by-cell navigation.

A spreadsheet can have indefinite numbers of columns with or without variable numbers of lines for different rows. I'm not actually going to implement a spreadsheet that grows indefinitely in two dimensions. I want to interact with databases in UI.

My tabular lists will have fixed numbers of columns and indefinite numbers of rows fetched dynamically from databases. The list widget can only handle a fixed number of lines for every row.

It seems I can combine table and viewport and list to get a horizontally scrollable tabular list widget. The list widget takes care of vertical dimension. My horizontally scrollable tabular list takes care of horizontal dimension.

jtdaugherty commented 1 year ago

It seems I can combine table and viewport and list to get a horizontally scrollable tabular list widget.

For what it's worth, you might need to fake horizontal scrolling yourself rather than use a horizontal viewport. Embedding a viewport in the list would work just fine, but you'd need a per-item viewport to make it work and you'd need to scroll them all horizontally to get the appearance you probably want. If you're using the tabular list demo as a basis for your work, since that implementation already needs to declare and use the width of each table column, you could use that information and track which "columns" are visible and show only a subset yourself to emulate a scrollable viewport.

amano-kenji commented 1 year ago

If I just wanted to render every column for each row, I can just put the list header and the list in a horizontal viewport and use visibility request on the chosen column. But, I wanted to render columns efficiently even if there are actually thousands of columns. If I used your algorithm for drawing List, I would have to go through lots of columns to calculate horizontal translation. That's not very efficient if there are many columns that have varying widths.

I used my internal intuition really hard today to figure out an efficient algorithm for culling and displaying columns. Since I didn't have any finished concept I could iterate on in the code, I had to iterate scrolling concepts in my mind.

After seeing how sc-im handles horizontal scrolling, I made conceptual breakthroughs.

I came up with the following data structure and the following algorithm. The algorithm is still subject to changes because it is still unfinished. I'm iterating the algorithm purely in my mind because I don't want to be slowed down by coding an unfinished concept.

data ScrollAnchor = Left Word | Right Word

data GenericTabularList n t e = {
  list :: GenericList n t e
, curCol :: Word
, scrollAnchor :: ScrollAnchor
, columnWidths :: V.Vector Word
}

With the algorithm, a tabular list can dynamically adapt to changing widget size and changing column sizes. It can render columns efficiently even if there are millions of columns. The time complexity for vector indexing is O(1).

If I change the type of columnWidths to c Word and combine it with t e, then a tabular list can potentially handle sparsely populated rows. This means GenericTabularList can potentially become a spreadsheet with a fixed height for each row. With the right data types for t and c, an efficient sparsely populated spreadsheet with as many columns as the maximum size of Word is possible.

For most cases, people just need densely populated tabular lists. TabularList should be able to handle such a use case.

I guess you can use my algorithm to enable GenericList to render variable numbers of lines for different rows and still get the correct rendering. But, since I don't see myself implementing microsoft excel, there is probably no need to implement a list with varying numbers of lines for different rows. Even sc-im shows one line for one row.

amano-kenji commented 1 year ago

I largely figured out an efficient column drawing algorithm. I need to figure out containers for columns and column widths.

Do you know good data structures for columns and column widths?

jtdaugherty commented 1 year ago

Do you know good data structures for columns and column widths?

I can't provide an answer without more information. What is a "column"? What kinds of operations do you need to perform on it?

amano-kenji commented 1 year ago

I think I will just show as many columns as the length of column widths. If a row has more columns than the length of column widths, the excess columns are not displayed. If it has less columns than the length of column widths, the rest is empty.

For a sparsely populated spreadsheet, one can use something like Map Word (Map Word cell) for rows and columns and something like Map Word Word for column widths. If Map Word Word returns Nothing, then a default column width should be used. In real code, data types that use those maps will be used. The actual data types for spreadsheet may also have a list of dense regions for efficiency. A spreadsheet software may allow users to define dense regions.

For product data types representing database tables, Vector Word is totally fine for column widths.

For both spreadsheet and product data types, I'm figuring out the best typeclasses for getting and setting columns. The number of columns is fixed for both spreadsheet and product data types.

jtdaugherty commented 1 year ago

Okay. I don't see a question in your response so I don't think there's much for me to add.

amano-kenji commented 1 year ago

I actually have a question that I may or may not answer myself, given enough time, but you can help accelerate the process.

When rendering a column, I want to get the text content from a product data type or a spreadsheet data type. A tabular list doesn't need to know how to manipulate data types behind columns. Manipulating data types behind tabular list would be a responsibility for application event handler. Tabular list only needs to get the text content for each column.

I was thinking of implementing something like this for getting the text content for each column.

type ColumnIndex = Word
type ColumnWidth = Word

-- a is the product data type or the spreadsheet row data type.
class RowInfo a where
  showColumn :: ColumnIndex -> ColumnWidth -> a -> Maybe String
  -- ColumnWidth is used to truncate text in order to fit the column width.
  columnWidth :: ColumnIndex -> a -> Maybe Word
  -- This method is used to get the untruncated column width.
  -- The untruncated column width can be used to increase column width.

data Row = Row Int Int Int

instance RowInfo Row where
  showColumn 0 w row = Just $ show $ row^._1
  showColumn 1 w row = Just $ show $ row^._2
  showColumn 2 w row = Just $ show $ row^._3
  showColumn _ _ _ = Nothing
  columnWidth 0 row = Just $ length $ show $ row^._1
  columnWidth 1 row = Just $ length $ show $ row^._2
  columnWidth 2 row = Just $ length $ show $ row^._3
  columnWidth _ _ = Nothing

Do you think this is good enough? Do you suggest any better approach? After studying List functions, I may change my mind.

jtdaugherty commented 1 year ago

The interface provided in the type class seems like it could work well enough. My only suggestion is to use a record type to carry the showColumn and columnWidth functions rather than a type class. Using a type class this way is very likely to create headaches later on when RowInfo constraints need to be captured or existentially quantified over. In those cases it becomes a huge pain to carry around a value whose type you don't know when you need to also carry a RowInfo constraint. The solution is easy: the type class can be converted to a record type declaration in a straightforward way:

data RowInfo a =
    RowInfo { showColumn :: ColumnIndex -> ColumnWidth -> a -> Maybe String
            , columnWidth :: ColumnIndex -> a -> Maybe Word
            }

and then carry around a RowInfo a as part of whatever state is already in terms of a. This approach basically does what the compiler does (manufactures a record of functions) and stores it explicitly rather than implicitly, but it has the additional benefit that you can treat the record type as malleable business logic rather than a universal behavior of the type which it may very well not be, depending on the application context.

amano-kenji commented 1 year ago

Should GenericTabularList n t e carry RowInfo e? Or, should application state carry RowInfo e and supply it to functions?

jtdaugherty commented 1 year ago

I suspect the best place for it is in GenericTabularList itself. Otherwise you run the risk of needing to pass the RowInfo a as an argument to one or more list-related functions, which could be cumbersome.

amano-kenji commented 1 year ago

Does vertical border between columns occupy extra space I need to worry about in the column drawing algorithm?

I need to create visual distinction between columns. Otherwise, they may seem joined together. In sc-im, adjacent columns seemed joined together.

jtdaugherty commented 1 year ago

If you want to draw vertical borders, then they'd take up one terminal column's worth of width (assuming you use vBorder). That means that the total width of your N visible columns would be (N-1) + SUM(N_i) where N_i is the width of the contents of column i.

amano-kenji commented 1 year ago

By the way, beam was bragging about its extensive use of typeclasses by saying beam uses finally tagless encoding which I don't understand exactly. I guess typeclass is a form of finally tagless encoding.

What do you know about finally tagless encoding? Is it something I need to worry about?

jtdaugherty commented 1 year ago

I've only heard that term. I couldn't tell you anything about it.

amano-kenji commented 1 year ago

Here's a generalized pseudo-code for GenericTabularList.

This accounts for column header at the top and row header at the left. To make the interface generalized enough for spreadsheet and database tables, I created IndexedContents data type.

type Index = Word
type Width = Word

data IndexedContents e = {
  get :: Index -> Maybe e
, set :: Index -> e -> IndexedContents e
, length :: Index
}

data RowHeader = RowHeader
  { contents :: IndexedContents String, width :: Width, newWidth :: Maybe Width }

data RowInfo e = RowInfo {
  showColumn :: Index -> Width -> e -> Maybe String
, columnWidth :: Index -> e -> Maybe Width
}

data GenericTabularList n t e = {
  list :: GenericList n t e
, rowInfo :: RowInfo e
, curCol :: Index
, columnWidths :: IndexedContents Width
, newColumnWidths :: Map Index Width
, colHeader :: Maybe (IndexedContents String)
, rowHeader :: Maybe RowHeader
}

drawTabularList :: GenericTabularLis n t e -> ... -> Widget n
-- These functions are called by drawTabularList
-- drawRowHeader, drawColHeader, and drawColumn have default implementations,
-- but users can pass their own implementations to drawTabularList
drawRowHeader :: Bool -> Index -> EventM n (GenericTabularList n t e) ()
drawColHeader :: Index -> EventM n (GenericTabularList n t e) ()
drawColumn :: Bool -> e -> Index -> EventM n (GenericTabularList n t e) ()

-- These functions can be called by drawRowHeader, drawColHeader, and drawColumn

-- setRowHeaderWidth sets rowHeader.newWidth
setRowHeaderWidth :: Width -> EventM n (GenericTabularList n t e) ()
-- setColumnWidth inserts (Index, Width) into newColumnWidth
setColumnWidth :: Index -> Width -> EventM n (GenericTabularList n t e) ()

If setRowHeaderWidth and setColumnWidth were called in user's implementations of drawRowHeader, drawColHeader, or drawColumn, then rowHeader.newWidth and newColumnWidth are applied and reset, and GenericTabularList is drawn again. This means GenericTabularList can be drawn multiple times before being shown on screen.

Do you suggest any better interface than what I just wrote above? I think IndexedContents is ugly. Also, storing new widths in GenericTabularList temporarily looks a bit ugly. The code is really just a rough draft.

jtdaugherty commented 1 year ago

Do you suggest any better interface than what I just wrote above? I think IndexedContents is ugly. Also, storing new widths in GenericTabularList temporarily looks a bit ugly. The code is really just a rough draft.

At this point I really couldn't say whether it could be improved. In my experience, designs like this almost always change when you get down to actually writing the application, and it's only then that you find out what your design's flaws or ergonomic issues are. And I haven't been following along with your design here because I would rather help with library issues as they arise rather than helping you design your application from the ground up. That's a lot of work! :)

amano-kenji commented 1 year ago

Okay. I will come back when I have issues with brick library.

Can you see this tabular list widget in brick library when it's finished? Tabular data like SQL database table and CSV are common.

jtdaugherty commented 1 year ago

Can you see this tabular list widget in brick library when it's finished?

No, but it seems to me like the sort of thing that would work really well as a third-party extension package similar to how brick-filetree was published!

amano-kenji commented 1 year ago

As I was writing actual code, I discovered that EventM doesn't have access to viewport or widget size and RenderM can't modify application state.

I was going to update data ScrollAnchor = AnchoredLeft n | AnchoredRight n according to viewport size, current value of scroll anchor, and current column. Obviously, RenderM can calculate a new value for scroll anchor, but can't assign the new scroll anchor to application state. EventM is able to modify application state but cannot calculate scroll anchor.

This means I am stuck with stateless rendering?

Without scroll anchor, the next thing I can think of is to save calculated offsets for all columns whenever column widths are modified. Calculating column offset every time is going to be inefficient. Saving new column offsets after column width changes is probably efficient enough, but I'm looking to squeeze more performance out of my design.

Is there anything I can take advantage of in brick's design to make the rendering more efficient than saving calculated offsets for all columns after changes to column widths? Or, can you allow application authors to modify application state in RenderM?

jtdaugherty commented 1 year ago

As I was writing actual code, I discovered that EventM doesn't have access to viewport or widget size and RenderM can't modify application state.

EventM can get viewport info with lookupViewport but that's only as of the last rendering. And yes, RenderM cannot modify the application state by design.

This means I am stuck with stateless rendering?

This is true and is by design. I hope this is not a surprise; if it isn't clear in the user guide then I'd be happy to make edits to make this clearer.

Is there anything I can take advantage of in brick's design to make the rendering more efficient than saving calculated offsets for all columns after changes to column widths?

I don't really know much about the approach you have been developing, but I would recommend you take a close look at the List implementation. That has some detailed comments that talk about how it makes use of Brick's built-in render-time viewport states while still maintaining efficiency. You might be able to do something similar in your use case.

amano-kenji commented 1 year ago

EventM can get viewport info with lookupViewport but that's only as of the last rendering.

Does this mean lookupViewport can return an outdated viewport size in vty resize event and possibly other events? This can result in graphical glitches if I rely on lookupViewport for scrolling calculation.

This is true and is by design. I hope this is not a surprise; if it isn't clear in the user guide then I'd be happy to make edits to make this clearer.

I think it's a good idea to explain brick's high-level design concepts.

That has some detailed comments that talk about how it makes use of Brick's built-in render-time viewport states while still maintaining efficiency. You might be able to do something similar in your use case.

drawListElements can have efficiency in offset calculation because the height for every list element is fixed. The width for any column can be different from that of any other. If I can't calculate scroll anchor in EventM, I can still calculate scroll offsets in EventM. The cost of calculating scroll offsets for the first time and after changing column widths is O(n).

O(n) is probably good enough because the time cost is O(n) only when column widths change, and microsoft excel tries not to automatically change the widths of some of its 2^14 = 16384 columns. For a few columns that have to fit inside viewport, O(n) is nothing. I think sc-im's 702 columns are numerous enough. If you need more columns, you are probably doing something wrong and should simplify your data model.

I still want to know whether I can avoid O(n) by calculating scroll anchor. If I don't have access to the real viewport size, I can't calculate scroll anchor. Do you know any way to get the real viewport size in resize event? Or, do you propose I should settle with paying the time cost of O(n) when I calculate scroll offsets for the first time and after changing column widths?

jtdaugherty commented 1 year ago

Does this mean lookupViewport can return an outdated viewport size in vty resize event and possibly other events? This can result in graphical glitches if I rely on lookupViewport for scrolling calculation.

Only "outdated" in the sense that it reflects what is currently on the screen, not what will happen in the next redraw. Basically, brick viewports are managed by the renderer, not by the user application. You can ask that the renderer scroll a viewport, but you don't get access to the underlying book-keeping.

I think it's a good idea to explain brick's high-level design concepts.

Given that the user guide is many tens of pages doing exactly this, I think I have this covered. But if you found something in the user guide or Haddock docs that was confusing w.r.t. rendering being a pure function, please let me know and I'll do some editing.

drawListElements can have efficiency in offset calculation because the height for every list element is fixed. The width for any column can be different from that of any other. If I can't calculate scroll anchor in EventM, I can still calculate scroll offsets in EventM. The cost of calculating scroll offsets for the first time and after changing column widths is O(n).

Calculating scrolling yourself outside of RenderM is just not going to work and is not how the library is designed. I directed you to the List code so you could begin to understand this.

O(n) is probably good enough because the time cost is O(n) only when column widths change, and microsoft excel tries not to automatically change the widths of some of its 2^14 = 16384 columns. For a few columns that have to fit inside viewport, O(n) is nothing. I think sc-im's 702 columns are numerous enough. If you need more columns, you are probably doing something wrong and should simplify your data model.

For what it's worth, while I understand the importance of performance considerations, I think worrying about it now is premature. I don't see enough evidence that you understand the library and how to use it, so I think focusing on performance will just not be very productive. I think your mental model of performance is not going to be correct until you understand how the library works, i.e. how the renderer and its scrolling viewports can be used. As part of that, I consider it an open question as to whether Brick can even be made to support the kind of application that you want to build! It may turn out that even with a deep understanding of Brick, this application will either not be possible to build or will require some Brick hacking to support. That's why I keep asking you to attempt an implementation: so we can figure out what issues come up and then I can determine whether those are a consequence of you not understanding, a consequence of the library design, a consequence of a missing library feature, etc.

amano-kenji commented 1 year ago

But if you found something in the user guide or Haddock docs that was confusing w.r.t. rendering being a pure function

I don't think the user guide explicitly tells anyone that RenderM cannot modify application state. But, reading the type of RenderM led to understanding that RenderM is pure. I just felt stupid for designing a widget with an assumption that RenderM could modify application state.

jtdaugherty commented 1 year ago

But, reading the type of RenderM led to understanding that RenderM is pure. I just felt stupid for designing a widget with an assumption that RenderM could modify application state.

I see, sorry to hear it. It's true that the type of RenderM can be understood not to modify the state at least because there is no application state type in the type signature; if RenderM was parameterized over the state type, that might be a clue that modification was possible (although even then you'd still need to look at the type classes that RenderM implemented -- such as MonadState -- to figure out whether modification was possible, and that might or might not be exposed as part of the public API).

jtdaugherty commented 1 year ago

Also, the other evidence for this, in case it helps, is that the only way to use the application state to draw is via the App's pure draw function, which produces only a Widget n (which is just a RenderM computation). The fact that it does not also produce an s is a clue that it cannot modify the state. (It could modify the s it was given, but it'd never get returned, thus no modification.)

jtdaugherty commented 1 year ago

It's true that the type of RenderM can be understood not to modify the state at least because there is no application state type in the type signature

Also, for what it's worth, this clue extends to the type Widget n, which can be understood to have nothing to do with any application state simply because no type is mentioned.

amano-kenji commented 1 year ago

I finished fleshing out MixedTabularList and GridTabularList. In particular, GridTabularList's stateless scroll calculation algorithm is tiny and fast and looks absolutely beautiful. MixedTabularList is just a non-scrollable list with each row having one or more columns. Column widths for MixedTabularList are dynamically calculated because every column is supposed to fit within viewport.

Calculation of dynamic column widths for MixedTabularList and scrolling information for GridTabularList is only done once for each rendering and not repeated for each row. Thus, it is going to be fast. The point is that they are fast.

I think it is going to crush microsoft excel in terms of rendering performance.

I just have to finish writing demos for two tabular lists and documentation. Before writing demos, I don't know whether they actually work.

amano-kenji commented 1 year ago

By the way, is the only way to force a specific width for a widget hLimit width (a horizontally greedy widget)?

I'm not using Table to give users flexibility. Anything I can do in a table, I can do with hLimit and padding functions.

I'm trying to minimize nested widgets for performance. Maybe, I'm dabbling with premature optimization.

jtdaugherty commented 1 year ago

By the way, is the only way to force a specific width for a widget hLimit width (a horizontally greedy widget)?

I'd say it's certainly the easiest and best way. Why do you ask?

amano-kenji commented 1 year ago

I'd say it's certainly the easiest and best way. Why do you ask?

I was trying to find a faster way than hLimit w (Greedy Widget). But, I think it's a premature optimization.

By the way, I want to give some columns fixed( percentage) widths and hide some of them automatically if the viewport is too narrow to show them. Can brick do it natively? Or, is it something that users have to calculate in their own applications? Mixed tabular list just lets users decide which columns to show with[Maybe Int] in narrow viewports for now. If I come up with an algorithm for that, users may not have to devise one for each application.

I don't know whether a generalized column-hiding algorithm can work for all or most applications.

jtdaugherty commented 1 year ago

I was trying to find a faster way than hLimit w (Greedy Widget). But, I think it's a premature optimization.

Do you mean faster as in "performance"? Or "ease of use of the API"? But either way, I would say it's not premature, it's just the wrong level of performance to worry about. (Not a first-order term in your performance equation, if you will.)

I want to give some columns fixed( percentage) widths and hide some of them automatically if the viewport is too narrow to show them. Can brick do it natively?

That's something that could only be done by writing a custom widget and using the rendering context to determine which of your columns could be rendered. But I could imagine it having an API like percentWidths :: [(Widget n, Int)] -> Widget n that behaves like hBox but is given a list of things to draw and their width percentages.

amano-kenji commented 1 year ago

Okay, either I come up with a generalized column width calculation algorithm, or users come with their own.

amano-kenji commented 1 year ago

I finished implementing brick-tabular-list. The demo programs work well. I just need to add documentation and publish it to hackage.

However, I had some issues I either fixed or worked around.

  1. The right side of each row was cropped by hBox before cropLeftBy cropped the left side. I worked around cropping of hBox by inserting setAvailableSize between hBox and cropLeftBy after calculating the amount of widths occupied by the columns given to hBox. Is there a better way to do this than inserting setAvailableSize between hBox and cropLeftBy? Calculating widths for setAvailableSize makes the code a little bit ugly.
  2. EvKey (KChar 'h') [MCtrl] was ignored.
  3. Pressing EvKey KPageUp [MMeta] or EvKey KPageDown [MMeta] kills my demo program. It seems these Alt keyboard shortcuts trigger VtyEvent (EvKey KEsc []) -> halt. Other brick programs also trigger escape key when I press Alt+PageUp or Alt+PageDown. htop doesn't trigger escape key when I press Alt+PageUp or Alt+PageDown. I think brick should handle Alt shortcuts better.
jtdaugherty commented 1 year ago

EvKey (KChar 'h') [MCtrl] was ignored.

C-h is equivalent to backspace, so it is not ignored, just reported as Backspace.

I can't answer your question in item (1) because I don't understand what you're talking about. I'd probably need to see a screenshot of what you're doing as well as the code in question.

As for (3), please open a new ticket about that.

amano-kenji commented 1 year ago

The code is

data VisibleColumns = NoColumn
  | CurrentColumn
  | AnchoredLeft { right :: Int }
  | MiddleColumns { left :: Int, right :: Int, offset :: Int, totalWidth :: Int }
  | AnchoredRight { left :: Int, offset :: Int, totalWidth :: Int }
  deriving Show

renderColumns ::
  GridTabularList n row cell rowH colH -> VisibleColumns -> (ColumnIndex -> Width -> Widget n) -> Widget n
renderColumns l vCs dC = Widget Greedy Fixed $ do
  c <- getContext
  let cWs = l ^. #widths % #row
      iH = l ^. #list % #listItemHeight
  render $ case vCs of
    NoColumn -> emptyWidget
    CurrentColumn -> dC (l ^. #currentColumn) $ c M.^. availWidthL
    AnchoredLeft right -> hBox $ zipWith dC [0..] $ toList $ S.take (right+1) cWs
    MiddleColumns {..} -> cropLeftBy offset $ setAvailableSize (totalWidth, iH) $
      hBox $ zipWith dC [left..] $ toList $ S.take (right-left+1) $ S.drop left cWs
    AnchoredRight {..} -> cropLeftBy offset $ setAvailableSize (totalWidth, iH) $
      hBox $ zipWith dC [left..] $ toList $ S.drop left cWs

The problematic part is MiddleColumns and AnchoredRight. If I eliminate setAvailableSize, I get this.

2023-02-02 07:47:232023-02-02 07:47:312023-02-02 07:47:41

With setAvailableSize, I get this.

2023-02-02 07:47:232023-02-02 07:48:242023-02-02 07:48:31

Without setAvailableSize, the columns get cropped at the right side by hBox before they reach cropLeftBy. Is there a way to guarantee enough horizontal space for hBox without setAvailableSize? I'd like to knock out totalWidth for beauty and simplicity. Is there a reason that hBox has to crop to context? Perhaps, cropping can be done later? Does cropping by hBox increase performance?

amano-kenji commented 1 year ago

I think the tabular list problem is solved. It's time to open a new issue.