From 3373b586845f99ca44a2f57214d99dd6c1fbc3df Mon Sep 17 00:00:00 2001 From: palexdev Date: Mon, 11 Mar 2024 11:54:20 +0100 Subject: [PATCH] :boom: Introducing VirtualizedFX logo :memo: Publish components documentation, specs and POCs to GitHub Wiki Signed-off-by: palexdev --- TODO.md | 37 +---- wiki/Grid.md | 94 ++++++++++++ wiki/Home.md | 18 +++ wiki/List.md | 75 ++++++++++ wiki/Table.md | 400 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 592 insertions(+), 32 deletions(-) create mode 100644 wiki/Grid.md create mode 100644 wiki/Home.md create mode 100644 wiki/List.md create mode 100644 wiki/Table.md diff --git a/TODO.md b/TODO.md index 36775ee..ffc61fc 100644 --- a/TODO.md +++ b/TODO.md @@ -1,35 +1,8 @@ ## General +- [ ] Replace `IntegerRage.expandRangeToSet(...)` with a class implementation that keeps track of removed indexes. + This way it is much more efficient. - [ ] Review APIs (classes, simplify as much as possible/re-organize at least) - -## List - -- [x] Items changed (property) -- [x] Items changed (inside list) -- [x] Init/Geometry change -- [x] Buffer change -- [x] Position change -- [x] Cell factory change -- [x] Fit to breadth change -- [x] Cell size change -- [x] Caching mechanism -- [x] Orientation change -- [x] Introduce spacing feature -- [x] Implement paginated variant - -## Grid - -- [x] Auto arrange -- [x] Viewport alignment -- [x] Items changed (property) -- [x] Items changed (inside list) -- [x] Init/Geometry change -- [x] Buffer change -- [x] Position change -- [x] Cell factory change -- [x] Cell size change -- [x] Caching mechanism -- [x] Vertical and horizontal spacing -- [ ] Implement paginated variant (?) - -## Table +- [ ] Rename cells to VFXxxx (?) +- [ ] Implement paginated Grid (?) +- [ ] Implement paginated Table \ No newline at end of file diff --git a/wiki/Grid.md b/wiki/Grid.md new file mode 100644 index 0000000..ee4c564 --- /dev/null +++ b/wiki/Grid.md @@ -0,0 +1,94 @@ +[//]: @formatter:off + +# VFXGrid + +## Specs + +- Style Class: `vfx-grid` +- Default Skin: `VFXGridSkin` +- Default Behavior: `VFXGridManager` + +### Properties + +| Properties | Description | Type | +|--------------------------|---------------------------------------------------------------------------------------|----------------| +| 1) items | Specifies the list containing the items to display | ObservableList | +| size(delegate) | Specifies the size of the list | Integer | +| empty(delegate) | Specifies whether the list is empty | Boolean | +| 2) cellFactory | Specifies the function used to build the Cells | Function | +| helper | Specifies the VFXGridHelper instance, a utility class which defines core computations | VFXGridHelper | +| helperFactory | Specifies the function used to build the VFXGridHelper | Supplier | +| 3) vPos | Specifies the viewport's vertical position | Double | +| 3) hPos | Specifies the viewport's horizontal position | Double | +| 4) virtualMaxX(delegate) | Specifies the total number of pixels alongside the x-axis | Double | +| 4) virtualMaxY(delegate) | Specifies the total number of pixels alongside the y-axis | Double | +| state | Specifies the state object, which represents the current grid's state | VFXGridState | +| needsViewportLayout | Specifies whether the viewport's layout needs to be computed | Boolean | + +### Styleable Properties + +| Property | Description | CSS Property | Type | Default Value | +|------------------|-------------------------------------------------------------------------------------------|-------------------------|------------------|----------------| +| 5) cellSize | Specifies the width and height of the cells | -vfx-cell-size | Size | [100.0, 100.0] | +| 6) columnsNum | Specifies the maximum number of columns the grid can have | -vfx-columns-num | Integer | 5 | +| alignment | Specifies the position of the viewport node inside the grid | -vfx-alignment | Pos | TOP_LEFT | +| vSpacing | Specifies the number of pixels between each row | -vfx-v-spacing | Double | 0.0 | +| hSpacing | Specifies the number of pixels between each column | -vfx-h-spacing | Double | 0.0 | +| clipBorderRadius | Specifies the radius of the clip applied to the grid (to avoid cells overflow) | -vfx-clip-border-radius | Double | 0.0 | +| cacheCapacity | Specifies the maximum number of cells to keep in the grid's cache when not needed anymore | -vfx-cache-capacity | Integer | 10 | +| 7) bufferSize | Specifies the extra number of cells to render (makes scrolling smoother) | -vfx-buffer-size | Enum(BufferSize) | Standard(2) | + +### Additional capabilities + +- Auto arrange (determine number of columns based on width) + +## Internals + +_**Architecture**_ +- Grid (Model): defines the capabilities/features +- Grid Skin (View): mostly handles layout +- Grid Manager (Behavior/Controller/Manager): reacts to inputs and properties changes to produce new states + +
+ +**How does it work?** +First and foremost, it's important to keep in mind that the Grid operates on a **one dimension data structure**, a simple list. +This means that it needs a fundamental value for it to function: **the number of columns**. This property can be implemented +in two ways: the first is to use the cell size and the container's width to compute the value dynamically; +the second is to simply let the user set the desired value. (both approaches are implemented btw) +This approaches allow us to make a very versatile skin. The viewport node can be sized according to the number of columns and +the cell size. The benefit is that through a property that specifies the alignment, we can position it as we desire in +the container. For example we could have a gallery with a certain number of columns,and have the viewport to be aligned +at the center, thus having equal spacing at the sides. +However, having a property specifying the number of columns, doesn't mean that the virtualization is disabled on the x-axis. +If the number of columns is high enough to surpass the needed number, then the container should only render the ones needed. +_So, how many values for the number of columns do we have?_ +- The **desired number** coming from the property +- The **visible number** coming from the container's width and cell size +- The **total number**, which is the visible number plus double the buffer +- The **final number** should be a value which is lesser than or equal to the desired one and the number of items. + +The **number of rows** depend on the number of columns. + +
+ +_**On changes/events/actions**_ +_Note 1: Numbers above and below are correlated ofc_ +_Note 2: Every **change** should produce and set a **new state**_ +_Note 3: The following "explanations" should not be considered the real implementation as in reality things may differ. +These are more like dev notes that helped me build the system by figuring out the core concepts/mechanisms._ + +0) Init depends on geometry (width/height). It's simple as it's enough to ensure that the viewport has the right amount of cells. + Old cells do not need to update, just disposal or caching (in case there are too many) + It's also important to ensure the positions are valid! +1) Very similar/same algorithm used for the List. +2) Recreate cells: Purge and dispose cached/old cells: No need to recompute positions and sizes. +3) Update cells. +4) This typically changes only as a consequence of others changing. +5) Ensure correct number of cells; Update 4; Ensure valid position; Layout. +6) When the number of columns changes, there are many others following. Update 4; Ensure valid positions; + Both the ranges (rows/columns) may change. Depending on the container's size, the number of cells may change too. + Because of this, maybe this could be treated as a geometry change. +7) Virtualized containers should have buffer cells both at the top and bottom of the viewport for a smoother scroll experience. + When the number of buffer cells changes, it's enough to act as if the geometry changed since only one thing can happen: + more or less cells than needed. \ No newline at end of file diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..8ee018d --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,18 @@ +
+

+ + + +

+ +*** + +## Welcome to the VirtualizedFX wiki! + +
+ +### Components + +* [List](https://github.com/palexdev/VirtualizedFX/wiki/List) +* [Grid](https://github.com/palexdev/VirtualizedFX/wiki/Grid) +* [Table](https://github.com/palexdev/VirtualizedFX/wiki/Table) \ No newline at end of file diff --git a/wiki/List.md b/wiki/List.md new file mode 100644 index 0000000..44ea06c --- /dev/null +++ b/wiki/List.md @@ -0,0 +1,75 @@ +[//]: @formatter:off + +# VFXList + +## Specs + +- Style Class: `vfx-list` +- Default Skin: `VFXListSkin` +- Default Behavior: `VFXListManager` + +### Properties + +| Property | Description | Type | +|--------------------------|---------------------------------------------------------------------------------------|----------------| +| 1) items | Specifies the list containing the items to display | ObservableList | +| size(delegate) | Specifies the size of the list | Integer | +| empty(delegate) | Specifies whether the list is empty | Boolean | +| 2) cellFactory | Specifies the function used to build the Cells | Function | +| helper | Specifies the VFXListHelper instance, a utility class which defines core computations | VFXListHelper | +| helperFactory | Specifies the function used to build the VFXListHelper | Function | +| 3) vPos | Specifies the viewport's vertical position | Double | +| 3) hPos | Specifies the viewport's horizontal position | Double | +| 4) virtualMaxX(delegate) | Specifies the total number of pixels alongside the x-axis | Double | +| 4) virtualMaxY(delegate) | Specifies the total number of pixels alongside the y-axis | Double | +| state | Specifies the state object, which represents the current list's state | VFXListState | +| needsViewportLayout | Specifies whether the viewport's layout needs to be computed | Boolean | + +### Styleable Properties + +| Property | Description | CSS Property | Type | Default Value | +|------------------|-------------------------------------------------------------------------------------------|-------------------------|-------------------|---------------| +| 5) cellSize | Specifies the width/height of the cells | -vfx-cell-size | Double | 32.0 | +| 6) spacing | Specifies the number of pixels between each cell | -vfx-spacing | Double | 0.0 | +| 7) bufferSize | Specifies the extra number of cells to render (makes scrolling smoother) | -vfx-buffer-size | Enum(BufferSize) | Standard(2) | +| 8) orientation | Specifies the list direction: vertical or horizontal | -vfx-orientation | Enum(Orientation) | Vertical | +| 9) fitToViewport | Specifies whether the cells should have the same size as the viewport | -vfx-fit-to-viewport | Boolean | true | +| clipBorderRadius | Specifies the radius of the clip applied to the list (to avoid cells overflow) | -vfx-clip-border-radius | Double | 0.0 | +| cacheCapacity | Specifies the maximum number of cells to keep in the list's cache when not needed anymore | -vfx-cache-capacity | Integer | 10 | + +## Internals + +_**Architecture**_ +- List (Model): defines the capabilities/features +- List Skin (View): mostly handles layout +- List Manager (Behavior/Controller/Manager): reacts to inputs and properties changes to produce new states + +
+ +_**On changes/events/actions**_ +_Note 1: Numbers above and below are correlated ofc_ +_Note 2: Every **change** should produce and set a **new state**_ +_Note 3: The following "explanations" should not be considered the real implementation as in reality things may differ. +These are more like dev notes that helped me build the system by figuring out the core concepts/mechanisms._ + +0) **Init/Geometry:** init depends on geometry (width/height) and orientation. It's simple as it's enough to ensure that + the viewport has the right amount of cells. Old cells do not need to update, just disposal or caching (in case there are too many) + It's also important to ensure the positions are valid! +1) Two changes can occur. **Internal** and **property**, however in theory it should be possible to treat them the same way. + When items change, there are several cases to consider. First and foremost one thing we can check is if after the change + the list is **empty**. + If the new size is **lesser** than before, then it's needed to invalidate the positions too, otherwise it's not needed. + After ensuring we are at the correct position, the following actions should be in theory very easy, since the new approach + will not take into account changes types and ranges anymore. + Compute the **new range**, iterate over it, get cells (reuse or create as needed), update them both for index and item, + proceed as usual. +2) Recreate cells: Purge and dispose cached/old cells: No need to recompute positions and sizes. +3) Update cells +4) This typically changes only as a consequence of others changing +5) Ensure correct number of cells; Update 4; Ensure valid position; Layout +6) Same as 5. +7) Virtualized containers should have buffer cells both at the top and bottom of the viewport for a smoother scroll experience. + When the number of buffer cells changes, it's enough to act as if the geometry changed since only one thing can happen: + more or less cells than needed. +8) Update 4; Cells can be reused, ensure they are updated; Layout +9) Update 4; Ensure valid position; Layout \ No newline at end of file diff --git a/wiki/Table.md b/wiki/Table.md new file mode 100644 index 0000000..168d621 --- /dev/null +++ b/wiki/Table.md @@ -0,0 +1,400 @@ +[//]: @formatter:off + +# VFXTable + +## Specs + +- Style Class: `vfx-table` +- Default Skin: `VFXTableSkin` +- Default Behavior: `VFXTableManager` +- Default columns: +- Default rows: + +### Properties + +| Property | Description | Type | +|--------------------------|----------------------------------------------------------------------------------------|----------------| +| 1) items | Specifies the list containing the items to display | ObservableList | +| size(delegate) | Specifies the size of the list | Integer | +| empty(delegate) | Specifies whether the list is empty | Boolean | +| 2) columns | The list containing the table's columns | ObservableList | +| 3) rowFactory | Specifies the function used to build the rows | Function | +| helper | Specifies the VFXTableHelper instance, a utility class which defines core computations | VFXTableHelper | +| helperFactory | Specifies the function used to build a VFXTableHelper instance | Function | +| 4) vPos | Specifies the viewport's vertical position | Double | +| 4) hPos | Specifies the viewport's horizontal position | Double | +| 5) virtualMaxX(delegate) | Specifies the total number of pixels alongside the x-axis | Double | +| 5) virtualMaxY(delegate) | Specifies the total number of pixels alongside the y-axis | Double | +| state | Specifies the state object, which represents the current table's state | VFXTableState | +| needsViewportLayout | Specifies whether the viewport's layout needs to be computed | Boolean | + +### Styleable Properties + +| Property | Description | CSS Property | Type | Default Value | +|----------------------|-------------------------------------------------------------------------------------------|---------------------------|-------------------------|---------------| +| 6) cellHeight | Specifies the height of each row (cells implicitly) | -vfx-cell-height | Double | 32.0 | +| 7) columnSize | Specifies the width and height of each column | -vfx-column-size | Size | [100.0, 32.0] | +| 8) columnsLayoutMode | Specifies whether the columns should have fixed sizes | -vfx-columns-layout-mode | Enum(ColumnsLayoutMode) | FIXED | +| 9) bufferSize | Specifies the extra number of rows and columns to render (makes scrolling smoother) | -vfx-buffer-size | Enum(BufferSize) | Standard(2) | +| clipBorderRadius | Specifies the radius of the clip applied to the grid (to avoid rows and columns overflow) | -vfx-clip-border-radius | Double | 0.0 | +| cellsCacheCapacity | Specifies the maximum number of cells to cache for each column (local to columns) | -vfx-cells-cache-capacity | Integer | 10 | +| rowsCacheCapacity | Specifies the maximum number of rows to cache (in table) | -vfx-rows-cache-capacity | Integer | 10 | + + +### Additional capabilities + +- Auto-size columns (VARIABLE mode only) + +## Internals + +_**Architecture**_ +- Table (Model): defines the capabilities/features +- Table Skin (View): defines the view, mostly handles layout. This is different compared to other components as the + `viewport` node is not enough. We need at least two nodes to arrange the columns and the rows. +- Table Manager (Behavior/Controller/Manager): reacts to inputs and properties changes to produce new states +- Table Row: responsible for containing and laying out cells, as well as update them as needed +- Table Column: header and separator for the cells; responsible for creating the cells + +
+ +**Proof of concept** +The table is much more complex compared to the list and the grid. Trivially, we could say that it is just a mix between +the two, because we have a 1D structure where each item is a **row**, but then in every row there is a cell for each column (2D layout). +However, practically speaking, things are not so simple. Although **rows** and **columns** are not new concepts, +in the table's context they become UI nodes. +In theory, a layout similar to the Grid's one could be implemented, but it actually would make some things much more complex to implement. +Some examples: +- Columns act as headers and separators for each type of cells, they can have fixed or variable width. +- Rows are containers for the cells and allow to easily implement features such as row highlighting/selection. +- Vertical scrolling is different here, because in such case we want to "move" the rows but **not** the columns. + +As a consequence of rows and columns being UI nodes in the table, they should **not be interfaces** but rather +**abstract classes**. + +### Columns + +The base class for the columns, `VFXTableColumn`, should extend `Labeled` and implement at least these core functionalities: +- A **factory** to produce cells. Each column should be responsible for producing cells for a certain type of data, + let's see a practical example: + ```java + // Our data model, a City + public record City(String name, int postalCode, int population) {} + + // The first column will show a "Name" label as the header, and produce cells that do the following mapping city -> city.name() + // The second column will show a "Postal Code" label as the header, and produce cells that do the following mapping city -> city.postalCode() + // The third column will show a "Population" label as the header, and produce cells that do the following mapping city -> city.population() + ``` + **Alternatively**, we could move the cell factory to the table, thus having **only one**, and base the data + mapping on the cell index. This solution however, may result in a **less versatile** and **less user-friendly** component. + I should **investigate** on this matter as this could change some core parts of the architecture. +- A **cache** to store cells that are not needed anymore. The cells cache is not one and is "local" to each column, this + is a consequence of the above property (**changes** in case of alternative scenario). +- The **table** instance. Each column should store the instance to the table they are assigned to. + For ease of use, the table should handle this property **automatically**, meaning that when columns are added/removed + from the table, this property should be set accordingly. + +### Rows + +As for the rows' base class, `VFXTableRow`, it's enough to extend `Region` as it is a mere container for cells, a custom skin +would be redundant. +However, from a "logical" point of view, rows are a bit more complex than that. In fact, since they manage the cells +they have to be stateful components, which means they should store information such as: +- The **columns range** which implicitly will give us the required cells to display +- The **index** to allow knowing in which row they are, as well as what item from the list is being displayed +- The **item** which is important to the cells to achieve the aforementioned mapping (see the above code example) +- The **cells** contained in itself. This in particular may be problematic depending on how we store them. + The simplest solution would be to use a **map** of type **[Integer -> Cell]**, but further below I'm going to discuss + the pros and cons, as well as an alternative way. + The **layout is unaffected** since it is **absolute** (so the actual column index is irrelevant), but it's crucial to + answer the following questions: + - What happens when columns are **added/removed**? + - What happens when a **permutation** happens (columns switching places)? +- The **table** instance. Each row should store the instance to the table they are assigned to. + For ease of use, the table should handle this property **automatically**, meaning that when rows are created/disposed, + this property should be set accordingly. + +
+ +#### _On changes/events/actions_ +_Note 1: Numbers above and below are correlated ofc_ +_Note 2: Every **change** should produce and set a **new state**_ +_Note 3: The following "explanations" should not be considered the real implementation as in reality things may differ. +These are more like dev notes that helped me build the system by figuring out the core concepts/mechanisms._ +_Note 4: changes starting with '[ R ]' are rows-related; changes starting with '[ C ]' are columns related; changes starting +with [ T ] are table-related meaning that they could affect the whole system or partially. "Related" may also simply mean: +"the computation/method will be in the relative class"._ + +0) **[ T ]** Init depends on geometry (width/height). It's simple as it's enough to ensure that the viewport has the right amount of rows and cells. + Old rows and cells do not need to update, just disposal or caching (in case there are too many) + It's also important to ensure the positions are valid! +1) **[ R ]** Very similar/same algorithm used for the list and the grid. The only difference here is that instead of working on the + cells directly, we work on the rows. Rows in common are moved to the new state, removed/unnecessary rows are reused/disposed. + When a row is updated, in particular its **item** property, it has to update its cells too by doing something like this: + ```java + cells.forEach(c -> c.updateItem(newItem)); + ``` +2) **[ R ]** This is complex; basically, there are two approaches I can think of: + 1) The first approach is simple but less efficient. When a change occurs, just reset the rows' state by clearing and + re-computing the cells' map. Since columns use a cache to store the cells, in theory the performance difference should + be negligible; although it's worth noting that if such changes occur frequently, then this difference **may** definitely be + impactful. + 2) The second approach is to use the `StateMap` to store the cells thus having a double mapping like this: + `[Integer -> Cell]` and `[Column -> Integer]` which resolves to `[Column -> Cell]`. + The below example shows in pseudocode how the algorithm could work doing it this way. +
+
+ PseudoCode Example + + ```java + /* + * Examples of changes + * Range: [0, 3] + * Starting list: [0:A, 1:B, 2:C, 3:D] + * + * Switch/Permutation: [0:C, 1:B, 2:A, 3:D] + * Add at 0: [0:Z, 1:A, 2:B, 3:C, 4:D] + * Remove at middle: [0:A, 1:C, 2:D] + */ + ColumnsMap map = ...; + IntegerRange range = ...; + Set expanded = IntegerRange.expandRangeToSet(range); + + for (Integer index : range) { + TableColumn column = table.getColumns().get(index); + Cell c = cells.remove(column); // !! + + // Commons + if (c != null) { + expanded.remove(index); + c.updateIndex(index); + map.put(index, column, c); // !! + continue; + } + + // New columns + map.put(index, column, getCell(index)); + } + + dispose(); + this.cells = map; + onCellsChanged(); // To update children + // Make sure to also call requestViewportLayout() where appropriate to update the layout + ``` +
+
+ It is very similar to the one used to manage items changes (for all containers: list/grid/table). Extracting common + columns is straightforward and efficient. When the cell is not found (null), it means that the column is "new" to the + range. Of course, those who are not outside the range cannot be reused, because as already mentioned before each column + build a "type" of cell. +
+ +
+
+ Important notes +
    +
  • + The StateMap is never reused, rather, every new state has a new map which is built as needed. To facilitate things, + the same should apply here.
    + The current map is going to be used to extract common columns and dispose the remaining entries.
    + A new map should be declared before starting the computation, and only after the disposal it should take + the place of the old one.
    + This is also needed because the StateMap implementation does not allow it. +
  • +
  • + This way, at the end, it's enough to update the children.
    + However, to ensure the cells are correctly laid out, a call to requestViewportLayout() must be done.
    + I believe the best place for this is to do it at the end of the method in the table manager.
    + (Cannot be done in the row as this would trigger the method multiple times) +
  • +
+
+
+ + In **both cases**, there's also another thing to consider. Remaining cells need to be disposed/cached. + If a column is removed from the table, I believe caching its cells should be fine. + In case the column is going to be **reused** in the same table or in another one in the future, then there would + be no need to build the cells. + In case the column is **not going to be reused** then we should make sure that the GC can do its job. + Which means that both the column and the cells should not hold any reference to any other component of + the system (item and table set to null should be enough). +
+ It's **important** to consider that changes in the columns list may lead to a different range. + This means that in the table manager's method a **new state** should be computed **if and only if** the range changes. + In any case, it's clear that the columns range must be computed everytime and passed to the method of each row. + (see how in the above pseudocode, the row needs the range for the computation) +
+ There's also an **edge** case to handle here. Since the columns store the instance to the table they are assigned to, + the table manager's method is also responsible for setting it for columns that are added/removed. + At init time, since the listener is added in the skin, the method needs to be called with a `null` parameter, in such + occasion, there's no need to update the rows of course. +3) **[ T ]** Same algorithm used for the other containers when the cell factory changed. + Current rows are to be **disposed**, which means that all the **cells** are going to be **cached** by the "parent" column. + When new rows are created the old cells will be reused. No need to recompute positions and sizes. +4) **[ R ]** When the **vertical** position changes, I believe the same algorithm used for the other containers can be used, + it's enough to update the rows with the items that are not in the viewport anymore (which implicitly means updating the cells). + When the **horizontal** position changes then we need to iterate over each row and update their columns range. + Unfortunately, the computation in this case is a little heavier on performance because, remember, **cells cannot be reused** + as each column produces its type of cells. +5) **[ T ]** In theory, these changes only when other kinds of changes occur. +6) **[ T ]** Ensure correct number of cells; Update 5; Ensure valid position; Layout +7) **[ T ]** Independently of the columns layout mode, it should be enough to compute both the columns and rows ranges + and check if they differ from the old ones. + If the **columns range** changes, then it's enough to iterate over the rows and give them the new range + (internally they will update themselves ofc). + If the **rows range** changes, it means that the **height** changed, so it may happen to have more or less rows than needed. + Simply add more or dispose unneeded ones. + In other words, I believe this could be treated as a geometry change. +8) **[ T ]** This may seem a complex one, but in reality it's simple, we just need to differentiate between two cases: + 1) **FIXED to VARIABLE:** positions are valid, rows range not going to change. The only things that changes is the + columns range because in **VARIABLE** mode, virtualization alongside the x-axis is disabled, which means that all + columns will be added to the viewport. Consequently, all their corresponding cells will be created and added to the container. +
+ 2) **VARIABLE to FIXED:** the vertical position is valid (since only the columns' width may have changed), and so is + the rows range too. Update 5; Invalidate positions (only hPos may change); Compute the new columns range and update the rows. +9) Virtualized containers should have buffer cells both at the top and bottom of the viewport for a smoother scroll experience. + When the number of buffer cells changes, it's enough to act as if the geometry changed since only one thing can happen: + more or less cells than needed. + +### When cell factories change... + +As already mentioned many times before, each column in the table is responsible for building its own "type" of cells, and +this is indeed a major difference when compared to other containers. +Because of this difference, process such cases is not as straightforward. Although it happens in the columns, the work +has to be delegated to the rows since they are the ones responsible for containing and mannaging the cells. +So, I can think of two ways to handle this (also adding a bit of history): +0) In _ancient times_ awful tricks and workarounds were used. A listener in the column's skin detected the change and + called a _delegate_ method in the table. The actual method was in the _manager_, which then delegated the update to + the current state. At this point, the state class would iterate over the rows and call the update method. + However, before doing so, the column's index was retrieved. To avoid performance issues, instead of using `indexOf(...)` + a `Map` was built in the table which kept the columns mapped by their position in the list. + A fine workaround indeed, but you can clearly see how messed up all of this is, summed it up in symbols, here's the flow: + `[Column Skin]->[Table]->[Manager]->[State]->[Column to index]->[Rows]` +1) The current new implementation uses an **event bus** to carry the change directly from the column to the rows. + When the factory changes, the column publishes an event carrying itself, the old and new factories. + By **subscribing** to such events, the rows can proceed with the update. +
+ The **good** thing is clearly having a shorter and much more clean flow: + `[Column]->[Event]->[Rows]` +
+ A possible **issue** could be the use of a middleman (the event bus) because we are "bypassing" the table manager. In other words, we are + **not creating a new state**, which in theory it's fine since the ranges do not change, but it comes with a + dilemma, discussed [here](#_to-generate-or-not-generate-a-new-state_). + It's also true that we could **subscribe to the event in the manager**, but still I prefer the solution below, as it + requires less code, less memory and it's more performant. +2) The **preferred** way would be to just use the table manager from the column as follows: + ```java + VFXTable table = getTable(); + VFXTableManager manager = table.getBehavior(); + manager.onCellFactoryChanged(column, oldFactory, newFactory); + ``` + The flow becomes: + `[Column]->[Manager]->[Rows]` + _(Not counting the table as we are just getting the manager instance)_ +
+ Pros: + - No need for an event bus, which doesn't harm performance in theory, but still... + - Each class does what it is responsible for, so we are honoring the SRP, good encapsulation. + +
+ +There is, however, a **common issue** shared by all implementations. +In all cases, we have the column as one of the inputs, but that is not enough to know which cell in each row is to be replaced. +So, we still would need to get its index by using `indexOf(...)` (**NOT feasible**, too heavy on performance). +**However**, this is only true in case cells are mapped as `[Integer -> Cell]`. +As discussed [above, at point 2](#_on-changeseventsactions_); in case we use a `StateMap`, we could retrieve and +manipulate the cells by using the **column as a key**. + +### Variable layout mode + +Handling layout while in **VARIABLE** mode is definitely a complex situation. There are two ways I can think of: +1) The **old implementation** did not use any special flag to check whether a layout was needed, but rather it was relying + on the JavaFX system. It was fine and performant anyway, as positions and sizes were cached and invalidated only in certain + occasions. So, it is indeed a feasible solution and while it's relatively simple to implement, it still requires a lot + of code and attention. +2) A **new solution** could be to use listeners. By adding a listener to its width property, the column can than inform + the table manager of the change and then ask the rows to update the layout. The **good** thing here is that we can + perform a **partial layout**. + Let's suppose we have a columns range of `[1, 7]`, and that column `5`'s width changed. + We can start processing the layout directly from index `5` to `7`. + Flow of execution: `[Column]->[Manager]->[Rows]` +
+
+ Possible pseudocode implementation + + ```java + // Column class + { + // Using MFXCore utility class because it's so cool! + VFXTable table = getTable(); + When.onChanged(widthProperty) + .condition(() -> table != null && table.getColumnsLayoutMode() == VARIABLE) + .then((ow, nw) -> table.getBehavior().onColumnWidthChanged(this, ow, nw)) + .listen(); // This needs to be disposed eventually + } + + // Manager class + { + VFXTable table = getNode(); + VFXTableHelper helper = table.getHelper(); + VFXTableState current = table.getState(); + helper.invalidatePos(); // Important! + current.getRows().values().forEach(r -> r.onColumnWidthChanged(column, oldWidth, newWidth)); + // Generate and set new state? + } + + // Row class + { + VFXTableHelper helper = getTable().getHelper(); + int startIndex = cells.get(column); // From the StateMap, resolving mapping of type [Column -> Integer] + for (int i = startIndex; i <= columnsRange.getMax(); i++) { + Node node = cells.get(startIndex).toNode(); + helper.partialLayout(startIndex, i, oldWidth, newWidth, node); + } + } + + // Helper class + { + // The column that changed only needs to be resized, the position is the same + if (startIndex == i) { + node.resize(newWidth, node.getLayoutBounds().getHeight()); + return; + } + + // Other columns have the same size as before, but need to be shifted on the x-axis + // In theory, the shift value is the difference between the new and old width + // IMPORTANT: we may want to snap the x position to avoid strange values (needs experimentation) + double wDiff = newWidth - oldWidth; + double newX = node.getLayoutBounds().getMinX() + wDiff; + node.relocate(newX, 0); + } + ``` +
+
+ + Some notes on this implementation: + 1) It's crucial to also invalidate the hPos, because when a column becomes smaller it may happen to have a smaller virtualMaxX value. + 2) The `partialLayout(...)` method is exclusive to the `VFXVariableTableHelper` + 3) It assumes the usage of a `StateMap` to store the cells + +// TODO additional optimizations. Visibility + +### _To generate or not generate a new state..._ + +For some changes it's not necessary to generate a new state object. A new one, for example, is tipically needed when one +or both the ranges change, but this is not always the case. +When a column changes its factory, or its width, for example, the state is technically the same because: in the first case +it's the rows' state that is changing, while in the latter it's a layout change. +
+ +#### _So, the question is, should we still generate and set a new state?_ +The idea would be to always communicate to the user that something in the system changed. That said, there are a series of +things to consider: +- A new flag should be added to the state class, a boolean value to tell that the state is technically a "clone" of + a previous one. +- The creation of the state should be fast. Rather than moving things around, it is desirable to create a new object with + the **references** from the other state. +- **In alternative** to the above solution, we could set the aforementioned flag to true and force the trigger of the + JavaFX's `fireValueChangedEvent()` method. +- There's a listener in the table's skin (well in every container's skin to be precise) that listens to state changes and + updates both the children's lists and the layout. In case a new state is a **"clone"** (check by using the aforementioned flag) + then we should not perform any operation in the listener.