mirror of
https://github.com/rivo/tview.git
synced 2026-04-26 21:35:54 +03:00
[GH-ISSUE #248] virtual Table view #question #192
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/tview#192
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @pihentagy on GitHub (Mar 11, 2019).
Original GitHub issue: https://github.com/rivo/tview/issues/248
Does the library support virtual table?
I'd like to build a simple log viewer with this lib, but the log is quite huge, so I don't think I'd like to load the whole logfile to memory.
@rivo commented on GitHub (Mar 16, 2019):
At this point, no. I guess one could experiment with using a
GetCell(row,column) *TableCellfunction that would allow you to return the cell content dynamically. But it would have to be random access. Right now, the user can jump to the beginning, to the end, page up/down, etc.. There are fixed rows/columns (at the beginning of the table). And we would need to know the total number of rows at any time. I'm not sure if you can provide that for your log. (If it's a simple file, it's probably difficult to implement without parsing it first and keeping it in memory.)If you do have random access to your log, I could look into this. But it could take a while and I don't know yet if I might hit any major roadblocks. (I haven't looked at the code in detail from this perspective.)
@pihentagy commented on GitHub (Mar 19, 2019):
I'm thinking of parsing the logfile in the background and "index" it properly.
So I would take care of the GetCell implementation details, and telling about the number of rows/columns.
Don't get how the fixed columns/rows complicate the implementation.
But to recap the big picture: could you separate the Table model and the table view? Then I'd like to implement my own model, and pass it to your view.
@pihentagy commented on GitHub (Mar 26, 2019):
I was thinking about parsing the logfile to an sqlite database in the background. Either way, I would just need a model interface to implement and a way to notify the viewer about the data changes.
@rivo commented on GitHub (May 13, 2019):
Apologies for the late reply. I was away for a few weeks. Is this issue still relevant to you?
@pihentagy commented on GitHub (May 17, 2019):
Relevant, but I can postpone this development (as this is a half-hobby project). I haven't dealt with the problem in the past weeks, but not having a model-view separation can be a dealbreaker for me.
@pckhoi commented on GitHub (Mar 20, 2021):
What is your current appetite to see this implemented? I'm needing this and might try to implement this myself but what are the criteria you are looking for?
@pihentagy commented on GitHub (Mar 24, 2021):
To be frank I lost interest in this feature.
But the original need was to be able to lazily access a potentially huge number of items and have it rendered. So there should be a callback provided to retrieve the element specified in a parameter. It implies that the columns of the table should not depend on the actual length of the (huge) table.
@pckhoi commented on GitHub (Mar 25, 2021):
I went ahead and produced a VirtualTable that is to my liking but I'm not sure if @rivo would be interested in seeing my pull request because there are some breaking changes:
func (c *TableCell) SetPosition(x, y, width int) *TableCellprintWithStyleshould becomePrintWithStyledecomposeString=>DecomposeStringiterateString=>IterateStringstringWidth=>StringWidthThis would imply perhaps remake the original Table primitive on top of VirtualTable primitive and all should be well.
@rivo commented on GitHub (Apr 26, 2021):
I'm not really sure I understand your suggestion. Maybe you can elaborate how this would work.
I looked at the code regarding implementing a virtual table and it doesn't look like there's a simple way. Tables are not read-only. There are all kinds of modifying functions like
SetCell(),RemoveRow(),RemoveColumn(),InsertRow(), andInsertColumn(). If we wanted to back the table by some virtual structure, it would have to implement all of these functions.Then, every time the table is redrawn, it would let the implementer know what rows can be displayed so it can adjust its mapping. While drawing, it would query the implementer for all cells in the visible row range. (If
SetEvaluateAllRows()was called withtrue, even invisible cells of the table would be queried.)Something like this:
Looking forward to your thoughts.
@pihentagy commented on GitHub (Apr 27, 2021):
Yes that's it, separate the handling of table model (I'd use TableModel as a type) and the table view.
@pckhoi commented on GitHub (Apr 27, 2021):
Your proposal assumes that the
TableContentinterface want to acceptTableCellas input. My table has to deal with 10s of million of rows and it doesn't even keep all the raw bytes of the data let alone the table cells. Instead it take in something that is close toio.ReadSeekerandio.ReaderAt- my point isVirtualTableshouldn't deal with how data is updated. Instead you can have aTablewidget that wrapVirtualTableand work with the interface that you proposed.My implementation of
VirtualTableis complicated enough so I'll only discuss how it is different from the currentTable:SetGetCellsFunc(getCell func(row, column int) []*TableCell)method which allow you to feed the list of cells to display at a row/col index. It doesn't keep anyTableCellof course.SetEvaluateAllRows.Some of my needs such as sub-columns maybe not relevant to most people but from this
VirtualTableit's possible to build the exact sameTablewidget.@rivo commented on GitHub (May 29, 2021):
I understand your use case and if we only looked at a subset of the current feature set of
Table, it would certainly be possible to implement it this way. However, I cannot simply ignore everything else that is in there and the requirements of other people who are using this package. So it's important to at least have a plan what to do with the functions I mentioned.Sure, you can choose not to implement functions like
RemoveRow(row int), that's up to you. But for those users who need these functions, I need to know what to do internally to make it work.SetGetCellsFunc(getCell func(row, column int) []*TableCell)is not enough, which is why I proposed theTableContentinterface.Along with a function
Table.SetTableContent(content TableContent), you can then define your own table (view). If this is not provided (probably in the majority of cases), the default implementation ofTableContentis used, which is what the table is doing now, i.e. no virtualization.Unless I misunderstood what you wrote, I don't think this is the case. In your case, you would implement
GetCell(),GetRowCount(), andGetColumnCount(). Because it's read-only, you would ignore the other functions. OnlyGetCell()would return aTableCellfor the requested cell. (It would be wise for you to cache these because the function is potentially called very often.)Does this make sense to you?
This is quite a bit of work so I want to make sure that we're on the same page.
@pckhoi commented on GitHub (Jun 1, 2021):
I think you misunderstood some of my suggestions. Yes, my
VirtualTableonly access cells viaSetGetCellsFunchowever it doesn't mean that I can't remove row. It just means that I won't be doing that by interacting withVirtualTabledirectly. This is about separation of concerns. Like I said,VirtualTableshould just worry about rendering cells, not how those cells contents can be changed. I mean look at Facebook's React, the store is separated from the component, if you want to update the component then you update the store, not the component directly. It absolutely doesn't mean thatVirtualTableis read-only. As I said,VirtualTabledoes not keep any table cell but discard them all once it is done with them, but that doesn't mean that whatever is supplying those cells cannot cache them.Again,
SetGetCellsFunc's main difference compared toTableContent's methods such asGetCellandSetCellisn't so much that it is "read-only". It is about pulling updates versus pushing updates. When I want to make changes to the table, I make changes to the data directly, then I just let theVirtualTableknow that it should now re-render. At which point it call the get cells func to get the cells that are currently in view. And it will result in cleaner code because all view-related logics are tucked into a few functions that deal with how to translate the object's state into the table cells wheneverVirtualTableask for them. Outside of those functions if I want to change the data I can change them directly, not having to sprinkleGetCellandSetCellall over my code.My ideal version of
TableContentinterface only have get functions, and I hope you understand by now that doesn't mean it is read-only:Also I'm reiterating that I want to split the current
Tablewidget intoVirtualTablewidget and a newTablewidget which wrapVirtualTableand maintain current behavior. The point is to create aVirtualTablewidget that only know how to display a table but otherwise stripped bare of any logic that are not strictly view-related. I was unsatisfied with some of the currentTable's behaviors such as selection so this would provide an avenue to make my own table widget. Perhaps this is out of scope for this issue. If it is let me know I will open another issue.@rivo commented on GitHub (Jun 24, 2021):
Let me ask you this way: Assuming I have a
Tablebacked by yourVirtualTable, as you described. In your opinion, what exactly is supposed to happen when I call this function?@pckhoi commented on GitHub (Jun 25, 2021):
I assume you want the interface for
Tableremain unchanged. Which means the table need to keep all of its cells in a two-dimension matrix along with row and column count. Which is something that the currentTableis already doing. The new function will be exactly the same as your currentTable.RemoveColumnfunction. The difference is, this newTabletype will not have any drawing logic whatsoever, it merely know how many rows there are, how many columns there are, what cell is at a certain position. TheVirtualTablewill take care of drawing,Tablecan tell it how many row and column there are viaSetShapeor evenSetShapeGetter, doesn't matter. TheTablecan also provide it with a function that it can call whenever it needs a cell at a certain position viaSetGetCellsFunc.And then when the
Table.Drawis called it can simply callVirtualTable.DrawbecauseVirtualTableis embedded.VirtualTablecalls its internal.getCellsfunction (which was provided viaSetGetCellsFunc) to get the cells that it need to draw. It will be very straight forward.@marcelocantos commented on GitHub (Jun 25, 2021):
Why not just implement
VirtualTableas a new widget type distinct fromTable? It could perhaps work by subclassingTableto leverage some of its code, but that would be an implementation detail.Tableitself would remain as is.@pckhoi commented on GitHub (Jun 25, 2021):
Because that means code duplication. Any drawing logic that
TablepossessVirtualTablewill surely have it as well. By makingTablerelying onVirtualTablefor drawing, any change to drawing logic only need to be made at one place.@pckhoi commented on GitHub (Jun 25, 2021):
But if you are simply asking whether
VirtualTableis a separate widget fromTablethen yes, it is a separate, lower-level widget.@pckhoi commented on GitHub (Jun 25, 2021):
As for what type should be embedded in what type, I thought the answer should be
VirtualTableas the embedded widget because it is lower-level.@marcelocantos commented on GitHub (Jun 25, 2021):
It would make sense to embed
VirtualTableifTablewasn't already coded without it. As it stands, going the other way is cleaner, since it doesn't require changing an existing implementation that wasn't designed with virtualisation of its contents in mind.I don't know the code base very well, but I suspect you could reuse most of the
Tablecode by embedding it inVirtualTable.@pckhoi commented on GitHub (Jun 25, 2021):
I was never aware that putting in the effort is one of the issues in discussion here. But if @rivo decides that he likes this direction then rest assure I'll open a PR with my own
VirtualTableandTableimplementation. I have already been using my ownVirtualTablerewrite for a while now.@rivo commented on GitHub (Jun 25, 2021):
Definitely yes. This is one of the core principles of this package that whenever possible, backwards compatibility is guaranteed.
@pckhoi I've gone over your comments many times. Maybe I'm looking at the problem from a completely different angle but I have trouble understanding the details of your suggestion, especially how to handle write operations when the
Tableclass itself doesn't have direct access to the data anymore (just via the read-only functions you suggested).I have a slight hunch that you want the
Tableclass to map visible cell positions (the ones on screen) to original cell positions (the ones in your own data structure). If that's the case, then for very large tables — and this is what the original request was about —Tablewould again create a very large matrix of position mappings. That wouldn't be desirable, in my opinion. Maybe you mean something else?But since you already have an implementation, how about you post a link to it? No need to submit a PR, a link to your code will do. I'll have a look. Maybe that will help me understand where you're coming from.
@pckhoi commented on GitHub (Jun 25, 2021):
My table would not need to maintain a large matrix of
TableCell, not even all the bytes are needed in memory. But it does maintain a cache of visible cells which get updated or removed depending on what positions are requested. In any case, look in this package there's a VirtualTable and other kinds of tables which all depend onVirtualTablefor actual drawing.@pckhoi commented on GitHub (Jun 25, 2021):
If you're asking about whether a new
Tableimplementation with identical interface that embedVirtualTable, would be better at handling large matrix of cells then no. Because the old interface dictates thatTablemust keep all of its cells in memory, visible or not. The point ofVirtualTablethen is to enable other kinds of table that can actually handle infinite amount of data to be built on top of it.@rivo commented on GitHub (Jun 25, 2021):
Thanks for providing a link to your implementation. It helped me understand what you've been talking about all along. (Maybe it's just me but from your descriptions alone, it was not clear that this is what you meant — too many questions unanswered.)
To be honest, though, I find this distinction between your
VirtualTableand theTablenot clear. Sure enough,VirtualTableimplements a virtual table but only the read-only parts. You say that if someone also wants write access, they should just modify the underlying data structure. Well, this means they then have to expose their data structure in a separate way somehow because there are no write methods inVirtualTable. Or, alternatively, they could just useTablewhich implements them using a specific implementation ofVirtualTable. But then they're stuck withTablewhich would be pretty much the way it is now and offers none of virtualization we want.I also don't think this distinction between
VirtualTableandTablewould be clear to many people. And ultimately, it's not consistent with how extensibility is implemented in thetviewpackage. Almost everytviewwidget provides specific functionality out of the box, and if you want to modify/extend that functionality, you "plug it in" with functions likeSetInputCapture()orSetAutocompleteFunc(). There is no separate classVirtualInputFieldfor extraInputFieldfunctionality.So all in all, I think what I outlined before is what I'll be going for after all. You will still be able to handle changes to the data on the implementation side, i.e. your "separation of concerns". For example, if you want to display a log that is continuously being written to, you're free to let the table access only the trailing rows of your log. You don't need to call or even implement
RemoveRow()to make that happen. But for those users who want to provide aTablewith all of its functionality but with a different data structure, they can do it using the interface described above.I understand that this is a very different direction from the one you've already taken and you may not be interested in this version. There are other people in this thread, though, that will hopefully benefit from this. (Also, I would say that things like sub-cells, the lack of the "evaluate all rows" flag, and a different selection functionality are out of scope for now anyway.)
Everyone, feel free to weigh in. I'm never quite sure if I missed a certain perspective that would make this a bad solution. But as of now, it looks like this will be the implementation.
@pckhoi commented on GitHub (Jun 26, 2021):
I see that you have decided on an outcome. I don't think I can change your mind but the VirtualTable does follow a consistent pattern of the tview package that is composability. Higher-level components can be composed of lower-level components. For example 99% of all users are never expected to use the Box component, yet it is still exposed. And this is a powerful pattern, adding functionalities using layers of components (which is very successful in Facebook's React ecosystem). Extensibility via plugins is certainly possible but it is a dying trend, just look at React and a slew of technologies that emulate it such as Web Component, Vue.js. None of them rely on plugins for extensibility, all rely on composability. Since tview is a view library I thought it would be best if it also learns from other successful view libraries. But I understand if you don't agree.
@pckhoi commented on GitHub (Jun 26, 2021):
And regarding modifying data away from the table. Again, this is a pattern borrowed from React: separation of state management and view logic. But anyway if you feel like it's just coming from a React fanboy then feel free to ignore me.
@rivo commented on GitHub (Jun 26, 2021):
Sure, React has its merits, and it's currently undoubtedly popular. Whether or not its model is the best of its kind is debatable. I've been programming with UI toolkits for 25 years, including React, and honestly, I wouldn't say React is the best I've ever used. In fact, when I started using it, I found it quite difficult to get it right due to its variety of concepts (e.g. mounting, updating, unmounting pre/post phases, states vs. props, and many others). I wanted
tviewto be very easy to get started with.What I didn't anticipate was that people would request a lot more complex functionality from
tviewthan what it already does. So when people needed to customize their widgets, I added ways to do it here and there, and it ended up being the plug-in kind of architecture. We're here now. And in packages like this one, I value consistency more than going with the latest trend. (Which, incidentally, is why I love Go.)As a side note, I don't see how my proposal mixes state management and view logic. Whatever your data structure is, a
Tablewon't know about it and if you make changes to your data structure,Tablewill reflect that. The question whether there have to be two different classes (VirtualTableandTable) rather than one (Table) is a different one, in my opinion.Oh, I would love to hide
Boxfrom users (and quite a few other classes/interfaces, by the way). But there are no abstract classes in Go. And I already mentioned elsewhere that I don't think replicating all methods in all classes is a good idea. (Also, I initially made the mistake of making too much public, and now I can't go back.)@marcelocantos commented on GitHub (Jun 27, 2021):
TableViewis fundamentally a read-only object. Changes are expressed by modifying the source data and tellingTableViewthat something changed so it can refresh the screen. Caching and localising the blast radius of a change (avoid reporting changes to a cell when it's not view) are key design questions.This is why I disagree with the idea of introducing virtualisation to
Table. It's a poor fit.My original thought was that, if
Tableisn't easy to use as the backend forVirtualTable, then perhaps some additions to the API could make it so. On reflection, however, I suspect that that's not practical. The design considerations are quite different. For instance, aVirtualTablecould in theory have a billion rows, each with its own size (Is size a per-row variable inTable?). You really don't want to create a billion placeholder rows, though. Moreover, you're much more likely to want to filter on a billion-row table than scroll through it (or at the very least filter before scrolling).I think
VirtualTablewould be a fine addition to the library, but I strongly suspect that it'll require its own implementation distinct fromTable. That said, once implemented,Tablecould be recast quite easily as a thin shim overVirtualTable.@rivo commented on GitHub (Aug 7, 2021):
So this is implemented now. But for now, it's in a separate branch "virtualtable" (see PR #634). I also added an example with
demos/table/virtualtable/main.go.As discussed previously,
Table's write functions are still there for compatibility reasons. But if you want to ignore them and instead modify the table on your end, i.e. makingTablea read-only object, you're free to do so. TheTableContentReadOnlystruct will help you reduce your boilerplate in that case.Give it a go and let me know how it works for you. Once it's stable, I'll merge the PR into the master branch.
@rivo commented on GitHub (Sep 20, 2021):
I've merged this branch into
mastersince there were no complaints.