fastpygrid 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastpygrid-0.1.0/.github/workflows/publish.yml +37 -0
- fastpygrid-0.1.0/.gitignore +19 -0
- fastpygrid-0.1.0/CMakeLists.txt +22 -0
- fastpygrid-0.1.0/LICENSE +21 -0
- fastpygrid-0.1.0/PKG-INFO +146 -0
- fastpygrid-0.1.0/README.md +125 -0
- fastpygrid-0.1.0/build.bat +14 -0
- fastpygrid-0.1.0/demos/_data.py +113 -0
- fastpygrid-0.1.0/demos/demo.bat +43 -0
- fastpygrid-0.1.0/demos/demo_gpu_qt.py +75 -0
- fastpygrid-0.1.0/demos/demo_gpu_tk.py +74 -0
- fastpygrid-0.1.0/demos/setup.bat +32 -0
- fastpygrid-0.1.0/docs/screenshot.png +0 -0
- fastpygrid-0.1.0/pyproject.toml +38 -0
- fastpygrid-0.1.0/requirements.txt +3 -0
- fastpygrid-0.1.0/scripts/benchmarks/bench_geometry.py +72 -0
- fastpygrid-0.1.0/scripts/tests/check_select.py +68 -0
- fastpygrid-0.1.0/scripts/tests/fuzz_coremodel.py +126 -0
- fastpygrid-0.1.0/src/__init__.py +13 -0
- fastpygrid-0.1.0/src/core/__init__.py +8 -0
- fastpygrid-0.1.0/src/core/_gpu/surface.cpp +281 -0
- fastpygrid-0.1.0/src/core/_gridstore/gridcore.cpp +428 -0
- fastpygrid-0.1.0/src/core/coremodel.py +359 -0
- fastpygrid-0.1.0/src/core/filter.py +91 -0
- fastpygrid-0.1.0/src/core/find.py +100 -0
- fastpygrid-0.1.0/src/core/geometry.py +341 -0
- fastpygrid-0.1.0/src/core/gpu.py +1531 -0
- fastpygrid-0.1.0/src/core/gridcontroller.py +367 -0
- fastpygrid-0.1.0/src/core/model.py +991 -0
- fastpygrid-0.1.0/src/core/paint.py +268 -0
- fastpygrid-0.1.0/src/core/render.py +77 -0
- fastpygrid-0.1.0/src/core/selection.py +180 -0
- fastpygrid-0.1.0/src/core/theme.py +43 -0
- fastpygrid-0.1.0/src/render/__init__.py +6 -0
- fastpygrid-0.1.0/src/render/gpu_qt.py +247 -0
- fastpygrid-0.1.0/src/render/gpu_tk.py +172 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Build the Windows wheel + sdist, then publish to PyPI when a GitHub Release is
|
|
4
|
+
# published (or run manually). Uses PyPI Trusted Publishing (OIDC) -- no token.
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
workflow_dispatch: # adds a manual "Run workflow" button in the Actions tab
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: windows-latest # needs MSVC + CMake to compile the DLLs (preinstalled)
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- name: Build wheel + sdist
|
|
19
|
+
run: |
|
|
20
|
+
python -m pip install --upgrade build
|
|
21
|
+
python -m build
|
|
22
|
+
- uses: actions/upload-artifact@v4
|
|
23
|
+
with:
|
|
24
|
+
name: dist
|
|
25
|
+
path: dist/*
|
|
26
|
+
|
|
27
|
+
publish:
|
|
28
|
+
needs: build
|
|
29
|
+
runs-on: ubuntu-latest # gh-action-pypi-publish only runs on Linux
|
|
30
|
+
permissions:
|
|
31
|
+
id-token: write # required for Trusted Publishing
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/download-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist
|
|
37
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.obj
|
|
4
|
+
*.exp
|
|
5
|
+
*.lib
|
|
6
|
+
*.dll
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
|
|
10
|
+
# build output (build.bat -> .py + .dll)
|
|
11
|
+
dist/
|
|
12
|
+
# cmake build tree
|
|
13
|
+
build/
|
|
14
|
+
|
|
15
|
+
# staged library copy (setup.bat)
|
|
16
|
+
demos/fastgrid/
|
|
17
|
+
|
|
18
|
+
# python packaging
|
|
19
|
+
*.egg-info/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.15)
|
|
2
|
+
project(fastgrid_native CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
if(NOT CMAKE_BUILD_TYPE)
|
|
7
|
+
set(CMAKE_BUILD_TYPE Release)
|
|
8
|
+
endif()
|
|
9
|
+
|
|
10
|
+
# The two native DLLs. Their .cpp #pragma comment(lib, ...) the Windows libs, so
|
|
11
|
+
# no target_link_libraries needed. No "lib" prefix -> ctypes loads them by name.
|
|
12
|
+
add_library(surface SHARED src/core/_gpu/surface.cpp)
|
|
13
|
+
add_library(gridcore SHARED src/core/_gridstore/gridcore.cpp)
|
|
14
|
+
set_target_properties(surface gridcore PROPERTIES PREFIX "")
|
|
15
|
+
|
|
16
|
+
# Install lays out the `fastgrid` package: each .dll beside its module, plus the
|
|
17
|
+
# .py sources (no .cpp). Destinations are under fastgrid/ so this works both for
|
|
18
|
+
# build.bat (--prefix dist -> dist/fastgrid) and the scikit-build-core wheel
|
|
19
|
+
# (prefix = wheel root -> fastgrid/ in site-packages).
|
|
20
|
+
install(TARGETS surface RUNTIME DESTINATION fastgrid/core/_gpu LIBRARY DESTINATION fastgrid/core/_gpu)
|
|
21
|
+
install(TARGETS gridcore RUNTIME DESTINATION fastgrid/core/_gridstore LIBRARY DESTINATION fastgrid/core/_gridstore)
|
|
22
|
+
install(DIRECTORY src/ DESTINATION fastgrid FILES_MATCHING PATTERN "*.py")
|
fastpygrid-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Flynn Crochon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastpygrid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fast, GPU-painted spreadsheet grid for tens of thousands of rows (Windows, Direct2D).
|
|
5
|
+
Keywords: grid,spreadsheet,table,gpu,direct2d,tkinter,qt
|
|
6
|
+
Author: Flynn Crochon
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
15
|
+
Project-URL: Homepage, https://github.com/flynncrochon/Fast-Python-Grid
|
|
16
|
+
Project-URL: Repository, https://github.com/flynncrochon/Fast-Python-Grid
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Provides-Extra: qt
|
|
19
|
+
Requires-Dist: PySide6; extra == "qt"
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Fast Python Grid
|
|
23
|
+
|
|
24
|
+
A fast, GPU-painted spreadsheet grid for tens of thousands of rows. Scroll,
|
|
25
|
+
select, filter and find stay instant because only the visible cells are ever
|
|
26
|
+
built. The grid logic lives in a GUI-free core; a single toolkit-neutral
|
|
27
|
+
Direct2D engine draws it, hosted by a thin Tk or Qt adapter.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
src/ fastgrid .py sources (no DLLs live here; becomes dist/fastgrid)
|
|
31
|
+
core/ model, geometry, selection, paint() -> display list, gpu.py (Direct2D engine)
|
|
32
|
+
_gpu/ surface.cpp
|
|
33
|
+
_gridstore/ gridcore.cpp
|
|
34
|
+
render/ gpu_tk.py (tkinter host) · gpu_qt.py (PySide6 host)
|
|
35
|
+
CMakeLists.txt builds the DLLs + installs the .py into dist/fastgrid/
|
|
36
|
+
build.bat runs CMake (configure/build/install) -> dist/fastgrid/ (the runnable library)
|
|
37
|
+
demos/ demo_gpu_tk.py · demo_gpu_qt.py · _data.py · setup.bat (stages demos/fastgrid + demos/.venv)
|
|
38
|
+
scripts/
|
|
39
|
+
tests/ check_select.py · fuzz_coremodel.py (import dist/fastgrid)
|
|
40
|
+
benchmarks/ bench_geometry.py (import dist/fastgrid)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`core.paint()` returns a display list (plain-data draw ops for the visible
|
|
44
|
+
cells); the engine just blits it. The host only owns the window and translates
|
|
45
|
+
events, so the Tk and Qt hosts behave identically:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
dl.cells = [(x, y, w, h, text, bg, fg, flags), ...] # ~visible cells, back-to-front
|
|
49
|
+
dl.overlays = [("line"|"vline"|"hline"|"ring"|"tri", ...)] # chrome drawn after cells
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`core.paint` decides every colour, position and z-order; the engine is ~"for
|
|
53
|
+
each cell fill a rect + draw text; for each overlay draw a line/rect/triangle".
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
| Requirement | Details |
|
|
60
|
+
|---|---|
|
|
61
|
+
| **Windows only** | The renderer is Direct2D (`_gpu/surface.dll`), so it needs Windows and a Direct3D 11-capable GPU (falls back to the WARP software device if there's no GPU). There is no macOS/Linux backend. |
|
|
62
|
+
| **Python 3.8+** | |
|
|
63
|
+
| **Tk host** | Standard library only (`tkinter`, ships with Python). |
|
|
64
|
+
| **Qt host** | Needs `PySide6` (`pip install PySide6`), the only dependency, and only if you use the Qt host. |
|
|
65
|
+
| **Native DLLs** | Not committed -- `build.bat` runs CMake (which finds MSVC itself) to compile them into `dist/fastgrid/`. `surface.dll` draws the Direct2D surface and `gridcore.dll` is an optional C++ data core (the model falls back to pure Python if it's missing). Needs CMake + MSVC on the machine. Run `build.bat` once (and after any `.cpp` or `.py` change), then run the demos. |
|
|
66
|
+
|
|
67
|
+
## Example
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from fastgrid.render.gpu_tk import make_sheet # tkinter host (stdlib only)
|
|
71
|
+
win = make_sheet(
|
|
72
|
+
["Ticker", "Company", "Sector", "Price"],
|
|
73
|
+
[["AAPL", "Apple Inc.", "Technology", "189.20"],
|
|
74
|
+
["XOM", "Exxon Mobil", "Energy", "104.10"]],
|
|
75
|
+
frozen_columns=2, # pin the first 2 columns against horizontal scroll
|
|
76
|
+
)
|
|
77
|
+
win.mainloop()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Double-click or type to edit. Enter/Tab commit, Ctrl+Z/Y undo/redo, Ctrl+C/V
|
|
81
|
+
copy/paste, Ctrl+A select-all, Ctrl+F find, ▼ on a header to filter/sort.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from fastgrid.render.gpu_qt import make_sheet # PySide6 host
|
|
85
|
+
win = make_sheet(headers, rows, frozen_columns=2)
|
|
86
|
+
win.mainloop() # aliases app.exec()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Build
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
build.bat
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Runs CMake to compile the DLLs and assemble the runnable library into
|
|
96
|
+
`dist/fastgrid/` (`.py` + `.dll` only). Needs CMake and MSVC. Re-run after any
|
|
97
|
+
`.cpp` or `.py` change, then `demos/setup.bat` to stage `demos/fastgrid` + `demos/.venv`.
|
|
98
|
+
|
|
99
|
+
## Run
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python demos/demo_gpu_tk.py # tkinter host, 100k rows
|
|
103
|
+
python demos/demo_gpu_qt.py # Qt host, same data
|
|
104
|
+
python demos/demo_gpu_tk.py --rows 500000 # stress it
|
|
105
|
+
python scripts/tests/check_select.py # selection-state-machine check
|
|
106
|
+
python scripts/tests/fuzz_coremodel.py # C++ data core vs pure-Python oracle
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Performance
|
|
110
|
+
|
|
111
|
+
Only the visible cells are built, so the row count barely matters. 10k rows
|
|
112
|
+
and 1M rows do the same work per frame.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
rows core paint()
|
|
116
|
+
10,000 0.13 ms
|
|
117
|
+
100,000 0.13 ms
|
|
118
|
+
1,000,000 0.13 ms
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The Direct2D surface redraws that frame on the GPU, vsync-capped (~60 fps), and
|
|
122
|
+
stays smooth while scrolling millions of rows.
|
|
123
|
+
|
|
124
|
+
## Features
|
|
125
|
+
|
|
126
|
+
The engine handles all of this, so both the Tk and Qt hosts get it for free.
|
|
127
|
+
|
|
128
|
+
| Feature | Details |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Frozen columns | Pin leading columns against horizontal scroll. |
|
|
131
|
+
| Pinned selectable header rows | Pass a list of lists as `headers` for a multi-row header; adjacent same-label cells in the upper rows merge into spanning group bands, e.g. `[["", "FY 2023", "FY 2023"], ["Ticker", "Q1", "Q2"]]`. |
|
|
132
|
+
| Excel-style selection | Click/drag/Ctrl/Shift, whole row/column, select-all, Ctrl+arrow block jumps. |
|
|
133
|
+
| In-cell editing | Double-click or type to edit; copy/paste/delete, undo/redo. |
|
|
134
|
+
| Filter & sort | Per-column value and text filters, A→Z/Z→A sort. |
|
|
135
|
+
| Find | Ctrl+F with prev/next, case, and selection scope. |
|
|
136
|
+
|
|
137
|
+
Per-cell styling and dropdowns, and thick black section dividers, are set on
|
|
138
|
+
the model (display only, positional):
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
model.set_cell_style(gr, col, fg="#c0392b", bold=True) # gr/col are GRID coords
|
|
142
|
+
model.set_cell_choices(gr, col, ["Buy", "Hold", "Sell"]) # editing offers a dropdown
|
|
143
|
+
model.set_vline(col) # thick rule on the RIGHT edge of a column
|
|
144
|
+
model.set_hline(gr) # thick rule on the BOTTOM edge of a grid row
|
|
145
|
+
model.set_readonly_col(col) # block edits/paste/delete in a column (still selectable)
|
|
146
|
+
```
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Fast Python Grid
|
|
2
|
+
|
|
3
|
+
A fast, GPU-painted spreadsheet grid for tens of thousands of rows. Scroll,
|
|
4
|
+
select, filter and find stay instant because only the visible cells are ever
|
|
5
|
+
built. The grid logic lives in a GUI-free core; a single toolkit-neutral
|
|
6
|
+
Direct2D engine draws it, hosted by a thin Tk or Qt adapter.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
src/ fastgrid .py sources (no DLLs live here; becomes dist/fastgrid)
|
|
10
|
+
core/ model, geometry, selection, paint() -> display list, gpu.py (Direct2D engine)
|
|
11
|
+
_gpu/ surface.cpp
|
|
12
|
+
_gridstore/ gridcore.cpp
|
|
13
|
+
render/ gpu_tk.py (tkinter host) · gpu_qt.py (PySide6 host)
|
|
14
|
+
CMakeLists.txt builds the DLLs + installs the .py into dist/fastgrid/
|
|
15
|
+
build.bat runs CMake (configure/build/install) -> dist/fastgrid/ (the runnable library)
|
|
16
|
+
demos/ demo_gpu_tk.py · demo_gpu_qt.py · _data.py · setup.bat (stages demos/fastgrid + demos/.venv)
|
|
17
|
+
scripts/
|
|
18
|
+
tests/ check_select.py · fuzz_coremodel.py (import dist/fastgrid)
|
|
19
|
+
benchmarks/ bench_geometry.py (import dist/fastgrid)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`core.paint()` returns a display list (plain-data draw ops for the visible
|
|
23
|
+
cells); the engine just blits it. The host only owns the window and translates
|
|
24
|
+
events, so the Tk and Qt hosts behave identically:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
dl.cells = [(x, y, w, h, text, bg, fg, flags), ...] # ~visible cells, back-to-front
|
|
28
|
+
dl.overlays = [("line"|"vline"|"hline"|"ring"|"tri", ...)] # chrome drawn after cells
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`core.paint` decides every colour, position and z-order; the engine is ~"for
|
|
32
|
+
each cell fill a rect + draw text; for each overlay draw a line/rect/triangle".
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
| Requirement | Details |
|
|
39
|
+
|---|---|
|
|
40
|
+
| **Windows only** | The renderer is Direct2D (`_gpu/surface.dll`), so it needs Windows and a Direct3D 11-capable GPU (falls back to the WARP software device if there's no GPU). There is no macOS/Linux backend. |
|
|
41
|
+
| **Python 3.8+** | |
|
|
42
|
+
| **Tk host** | Standard library only (`tkinter`, ships with Python). |
|
|
43
|
+
| **Qt host** | Needs `PySide6` (`pip install PySide6`), the only dependency, and only if you use the Qt host. |
|
|
44
|
+
| **Native DLLs** | Not committed -- `build.bat` runs CMake (which finds MSVC itself) to compile them into `dist/fastgrid/`. `surface.dll` draws the Direct2D surface and `gridcore.dll` is an optional C++ data core (the model falls back to pure Python if it's missing). Needs CMake + MSVC on the machine. Run `build.bat` once (and after any `.cpp` or `.py` change), then run the demos. |
|
|
45
|
+
|
|
46
|
+
## Example
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from fastgrid.render.gpu_tk import make_sheet # tkinter host (stdlib only)
|
|
50
|
+
win = make_sheet(
|
|
51
|
+
["Ticker", "Company", "Sector", "Price"],
|
|
52
|
+
[["AAPL", "Apple Inc.", "Technology", "189.20"],
|
|
53
|
+
["XOM", "Exxon Mobil", "Energy", "104.10"]],
|
|
54
|
+
frozen_columns=2, # pin the first 2 columns against horizontal scroll
|
|
55
|
+
)
|
|
56
|
+
win.mainloop()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Double-click or type to edit. Enter/Tab commit, Ctrl+Z/Y undo/redo, Ctrl+C/V
|
|
60
|
+
copy/paste, Ctrl+A select-all, Ctrl+F find, ▼ on a header to filter/sort.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from fastgrid.render.gpu_qt import make_sheet # PySide6 host
|
|
64
|
+
win = make_sheet(headers, rows, frozen_columns=2)
|
|
65
|
+
win.mainloop() # aliases app.exec()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Build
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
build.bat
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Runs CMake to compile the DLLs and assemble the runnable library into
|
|
75
|
+
`dist/fastgrid/` (`.py` + `.dll` only). Needs CMake and MSVC. Re-run after any
|
|
76
|
+
`.cpp` or `.py` change, then `demos/setup.bat` to stage `demos/fastgrid` + `demos/.venv`.
|
|
77
|
+
|
|
78
|
+
## Run
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python demos/demo_gpu_tk.py # tkinter host, 100k rows
|
|
82
|
+
python demos/demo_gpu_qt.py # Qt host, same data
|
|
83
|
+
python demos/demo_gpu_tk.py --rows 500000 # stress it
|
|
84
|
+
python scripts/tests/check_select.py # selection-state-machine check
|
|
85
|
+
python scripts/tests/fuzz_coremodel.py # C++ data core vs pure-Python oracle
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Performance
|
|
89
|
+
|
|
90
|
+
Only the visible cells are built, so the row count barely matters. 10k rows
|
|
91
|
+
and 1M rows do the same work per frame.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
rows core paint()
|
|
95
|
+
10,000 0.13 ms
|
|
96
|
+
100,000 0.13 ms
|
|
97
|
+
1,000,000 0.13 ms
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The Direct2D surface redraws that frame on the GPU, vsync-capped (~60 fps), and
|
|
101
|
+
stays smooth while scrolling millions of rows.
|
|
102
|
+
|
|
103
|
+
## Features
|
|
104
|
+
|
|
105
|
+
The engine handles all of this, so both the Tk and Qt hosts get it for free.
|
|
106
|
+
|
|
107
|
+
| Feature | Details |
|
|
108
|
+
|---|---|
|
|
109
|
+
| Frozen columns | Pin leading columns against horizontal scroll. |
|
|
110
|
+
| Pinned selectable header rows | Pass a list of lists as `headers` for a multi-row header; adjacent same-label cells in the upper rows merge into spanning group bands, e.g. `[["", "FY 2023", "FY 2023"], ["Ticker", "Q1", "Q2"]]`. |
|
|
111
|
+
| Excel-style selection | Click/drag/Ctrl/Shift, whole row/column, select-all, Ctrl+arrow block jumps. |
|
|
112
|
+
| In-cell editing | Double-click or type to edit; copy/paste/delete, undo/redo. |
|
|
113
|
+
| Filter & sort | Per-column value and text filters, A→Z/Z→A sort. |
|
|
114
|
+
| Find | Ctrl+F with prev/next, case, and selection scope. |
|
|
115
|
+
|
|
116
|
+
Per-cell styling and dropdowns, and thick black section dividers, are set on
|
|
117
|
+
the model (display only, positional):
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
model.set_cell_style(gr, col, fg="#c0392b", bold=True) # gr/col are GRID coords
|
|
121
|
+
model.set_cell_choices(gr, col, ["Buy", "Hold", "Sell"]) # editing offers a dropdown
|
|
122
|
+
model.set_vline(col) # thick rule on the RIGHT edge of a column
|
|
123
|
+
model.set_hline(gr) # thick rule on the BOTTOM edge of a grid row
|
|
124
|
+
model.set_readonly_col(col) # block edits/paste/delete in a column (still selectable)
|
|
125
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM Build the runnable fastgrid library into dist\fastgrid : the .py sources plus
|
|
3
|
+
REM freshly compiled .dll (via CMake), nothing else. CMake finds MSVC itself.
|
|
4
|
+
REM Run after changing any .py or .cpp, then run the demos against dist\fastgrid.
|
|
5
|
+
setlocal
|
|
6
|
+
set "ROOT=%~dp0"
|
|
7
|
+
|
|
8
|
+
if exist "%ROOT%dist" rmdir /s /q "%ROOT%dist"
|
|
9
|
+
cmake -S "%ROOT%." -B "%ROOT%build" -DCMAKE_BUILD_TYPE=Release || exit /b 1
|
|
10
|
+
cmake --build "%ROOT%build" --config Release || exit /b 1
|
|
11
|
+
cmake --install "%ROOT%build" --prefix "%ROOT%dist" --config Release || exit /b 1
|
|
12
|
+
|
|
13
|
+
echo [build] dist -^> %ROOT%dist\fastgrid
|
|
14
|
+
exit /b 0
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Shared sample data for the demos + benchmarks. Pure data: each caller puts
|
|
2
|
+
fastgrid on sys.path itself (demos use demos/fastgrid, scripts use dist/)."""
|
|
3
|
+
|
|
4
|
+
HEADERS = ["Ticker", "Company", "Sector", "Price", "Chg%", "Volume", "Note"]
|
|
5
|
+
SECTORS = ["Technology", "Energy", "Finance", "Health", "Consumer", "Utilities"]
|
|
6
|
+
COL_W = [110, 190, 155, 90, 80, 110, 90] # Sector (idx 2) widened for its ▼ dropdown arrow
|
|
7
|
+
|
|
8
|
+
# A batch of extra columns so the demo scrolls horizontally (exercises column
|
|
9
|
+
# resize / autofit). 8 quarters of revenue + a few text fields.
|
|
10
|
+
QUARTERS = ["%s %d" % (q, y) for y in (2023, 2024) for q in ("Q1", "Q2", "Q3", "Q4")]
|
|
11
|
+
HEADERS += QUARTERS + ["Analyst", "Rating", "Country", "Notes"]
|
|
12
|
+
COL_W += [95] * len(QUARTERS) + [140, 120, 120, 260] # Rating widened for its ▼ dropdown arrow
|
|
13
|
+
|
|
14
|
+
# Two-row grouped header: a band of "FY 2023"/"FY 2024" spanning each year's
|
|
15
|
+
# quarters above the field names (adjacent same-label cells merge into one
|
|
16
|
+
# spanning cell). Pass GROUPED_HEADERS instead of HEADERS to exercise it.
|
|
17
|
+
GROUPS = ([""] * 7 + ["FY %d" % y for y in (2023, 2024) for _q in range(4)]
|
|
18
|
+
+ [""] * 4)
|
|
19
|
+
GROUPED_HEADERS = [GROUPS, HEADERS]
|
|
20
|
+
RATINGS = ["Buy", "Hold", "Sell", "Strong Buy", "Underweight"]
|
|
21
|
+
COUNTRIES = ["USA", "Germany", "Japan", "United Kingdom", "South Korea", "Brazil"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def gen_rows(n):
|
|
25
|
+
return [[
|
|
26
|
+
"TIK%05d" % i,
|
|
27
|
+
"Company %d Inc." % i,
|
|
28
|
+
SECTORS[i % len(SECTORS)],
|
|
29
|
+
"%.2f" % (10 + (i * 7 % 9000) / 10.0),
|
|
30
|
+
"%+.2f" % (((i * 13) % 800 - 400) / 100.0),
|
|
31
|
+
str((i * 3779) % 5_000_000),
|
|
32
|
+
"watch" if i % 17 == 0 else "",
|
|
33
|
+
*("%.1fM" % (((i * (q + 7)) % 9000) / 10.0) for q in range(len(QUARTERS))),
|
|
34
|
+
"Analyst %d" % (i % 40),
|
|
35
|
+
RATINGS[i % len(RATINGS)],
|
|
36
|
+
COUNTRIES[i % len(COUNTRIES)],
|
|
37
|
+
"Longer free-text note for row %d to show autofit clipping." % i if i % 5 == 0 else "",
|
|
38
|
+
] for i in range(n)]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def rows_arg(argv, default=100_000):
|
|
42
|
+
return int(argv[argv.index("--rows") + 1]) if "--rows" in argv else default
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def style_demo(model, lo=None, hi=None):
|
|
46
|
+
"""Data-driven per-cell styling so the demos exercise fg / bold / bg:
|
|
47
|
+
Chg% red/green by sign, 'Strong Buy' ratings bold-green, 'watch' notes flagged
|
|
48
|
+
amber. Optional [lo, hi) GRID-row range so stream_styles() can apply it in
|
|
49
|
+
chunks; defaults to the whole dataset."""
|
|
50
|
+
chg, rating, note = HEADERS.index("Chg%"), HEADERS.index("Rating"), HEADERS.index("Note")
|
|
51
|
+
lo = model.header_rows if lo is None else lo
|
|
52
|
+
hi = model._real_rows() if hi is None else hi
|
|
53
|
+
for gr in range(lo, hi):
|
|
54
|
+
v = model.cell(gr, chg)
|
|
55
|
+
if v:
|
|
56
|
+
model.set_cell_style(gr, chg, fg="#c0392b" if v.startswith("-") else "#1e8449", bold=True)
|
|
57
|
+
if model.cell(gr, rating) == "Strong Buy":
|
|
58
|
+
model.set_cell_style(gr, rating, fg="#1e8449", bold=True)
|
|
59
|
+
if model.cell(gr, note) == "watch":
|
|
60
|
+
model.set_cell_style(gr, note, bg="#fff3b0")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def stream_styles(win, chunk=4000):
|
|
64
|
+
"""Apply the per-cell style pass in chunks AFTER the first frame, so the grid
|
|
65
|
+
shows instantly and never freezes on load. The first chunk covers the visible
|
|
66
|
+
rows, so what you see is styled within a frame; the rest streams in behind it."""
|
|
67
|
+
m, view = win.model, win.grid_view
|
|
68
|
+
total = m._real_rows()
|
|
69
|
+
|
|
70
|
+
def step(lo):
|
|
71
|
+
style_demo(m, lo, min(lo + chunk, total))
|
|
72
|
+
m.changed()
|
|
73
|
+
if lo + chunk < total:
|
|
74
|
+
view.after(1, lambda: step(lo + chunk))
|
|
75
|
+
|
|
76
|
+
view.after(0, lambda: step(m.header_rows))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Stress dropdown: 1000 options, most far wider than any column -- exercises the
|
|
80
|
+
# popup-widening (list grows to the text) and large option lists. The whole column
|
|
81
|
+
# shares one interned tuple, so this costs one list, not one copy per row.
|
|
82
|
+
_LONG = ("Comprehensive multi-word option label that is far wider than the column",
|
|
83
|
+
"Another lengthy descriptive choice overflowing the narrow cell by a lot",
|
|
84
|
+
"Short",
|
|
85
|
+
"Medium-length option text")
|
|
86
|
+
LONG_OPTS = ["%04d — %s" % (i, _LONG[i % len(_LONG)]) for i in range(1000)]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def lines_demo(model):
|
|
90
|
+
"""Thick black section dividers: a vertical rule right of the Chg% column
|
|
91
|
+
(splits the identity/price block from the quarterly financials) and a couple
|
|
92
|
+
of horizontal rules to band the data. Positional -- they mark a fixed place."""
|
|
93
|
+
model.set_vline(HEADERS.index("Chg%"))
|
|
94
|
+
model.set_hline(10) # under grid row 10 (a data row)
|
|
95
|
+
model.set_hline(25)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def readonly_demo(model):
|
|
99
|
+
"""Lock the Ticker + Price columns: still selectable and copyable, but edits,
|
|
100
|
+
paste and delete are rejected (try double-clicking a Ticker cell -- no editor)."""
|
|
101
|
+
model.set_readonly_col(HEADERS.index("Ticker"))
|
|
102
|
+
model.set_readonly_col(HEADERS.index("Price"))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def choices_demo(model):
|
|
106
|
+
"""Make Sector/Rating dropdowns, plus a 1000-option long-text dropdown on the
|
|
107
|
+
narrow Note column (to exercise popup widening). Same bulk-before-build note as
|
|
108
|
+
style_demo."""
|
|
109
|
+
sector, rating, note = (HEADERS.index("Sector"), HEADERS.index("Rating"),
|
|
110
|
+
HEADERS.index("Note"))
|
|
111
|
+
model.set_col_choices(sector, SECTORS) # whole-column dropdowns: O(1), not per row
|
|
112
|
+
model.set_col_choices(rating, RATINGS)
|
|
113
|
+
model.set_col_choices(note, LONG_OPTS)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM Launch the fastgrid Tk or Qt GPU demo using the project's .venv (Python 3.10).
|
|
3
|
+
REM Usage: demo.bat -> prompts for tk or qt
|
|
4
|
+
REM demo.bat qt -> runs the Qt (PySide6) demo
|
|
5
|
+
REM demo.bat tk -> runs the Tk demo
|
|
6
|
+
REM demo.bat qt --rows 500000 -> extra args pass through to the demo
|
|
7
|
+
|
|
8
|
+
setlocal
|
|
9
|
+
REM This bat lives in demos/, so ROOT is its parent (the repo root).
|
|
10
|
+
for %%I in ("%~dp0..") do set "ROOT=%%~fI\"
|
|
11
|
+
set "PY=%~dp0.venv\Scripts\python.exe"
|
|
12
|
+
|
|
13
|
+
if not exist "%PY%" (
|
|
14
|
+
echo [demo] demos\.venv not found. Run setup.bat first.
|
|
15
|
+
exit /b 1
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
set "TARGET=%~1"
|
|
19
|
+
if /i "%TARGET%"=="qt" goto run
|
|
20
|
+
if /i "%TARGET%"=="tk" goto run
|
|
21
|
+
|
|
22
|
+
set /p TARGET=Which demo? [tk/qt]:
|
|
23
|
+
if /i not "%TARGET%"=="qt" if /i not "%TARGET%"=="tk" (
|
|
24
|
+
echo [demo] Unknown demo "%TARGET%". Choose tk or qt.
|
|
25
|
+
exit /b 1
|
|
26
|
+
)
|
|
27
|
+
set "REST="
|
|
28
|
+
goto launch
|
|
29
|
+
|
|
30
|
+
:run
|
|
31
|
+
REM Drop the first arg (tk/qt); keep the rest to pass through.
|
|
32
|
+
shift
|
|
33
|
+
set "REST="
|
|
34
|
+
:collect
|
|
35
|
+
if "%~1"=="" goto launch
|
|
36
|
+
set "REST=%REST% %1"
|
|
37
|
+
shift
|
|
38
|
+
goto collect
|
|
39
|
+
|
|
40
|
+
:launch
|
|
41
|
+
echo [demo] Launching %TARGET% demo...
|
|
42
|
+
"%PY%" "%ROOT%demos\demo_gpu_%TARGET%.py"%REST%
|
|
43
|
+
exit /b %errorlevel%
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Direct2D/GPU demo under a Qt (PySide6) host -- same engine as demo_gpu_tk.py.
|
|
2
|
+
|
|
3
|
+
python demos/demo_gpu_qt.py # 100k rows on the GPU surface
|
|
4
|
+
python demos/demo_gpu_qt.py --rows 500000 # stress it
|
|
5
|
+
|
|
6
|
+
Tabs across the top open separate whole sheets, each with different options, so you
|
|
7
|
+
can compare them live. The "Uncapped" tabs scroll past the last row/column into
|
|
8
|
+
empty space (Excel-style): the scrollbar thumb shrinks as you overscroll and snaps
|
|
9
|
+
back when you scroll in again -- unless you typed out there, which grows the sheet.
|
|
10
|
+
|
|
11
|
+
Proves the toolkit-neutral GpuEngine runs unchanged under Qt. Build the DLL once:
|
|
12
|
+
python -m fastgrid.core.gpu --build
|
|
13
|
+
"""
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
# _data.py and fastgrid/ both live next to this file (fastgrid staged by setup.bat).
|
|
17
|
+
from _data import (HEADERS, COL_W, gen_rows, rows_arg, stream_styles, choices_demo,
|
|
18
|
+
lines_demo, readonly_demo)
|
|
19
|
+
|
|
20
|
+
# Each tab = a whole separate sheet with its own scroll-cap options.
|
|
21
|
+
SHEETS = [
|
|
22
|
+
("Capped (default)", {}),
|
|
23
|
+
("Uncapped rows", dict(uncap_rows=True)),
|
|
24
|
+
("Uncapped rows + cols", dict(uncap_rows=True, uncap_cols=True)),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _add_sheet(tabs, title, headers, rows, col_w, scale, lib, **opts):
|
|
29
|
+
"""Build one grid (its own model + engine) into a fresh Qt tab page."""
|
|
30
|
+
from PySide6 import QtWidgets
|
|
31
|
+
from fastgrid.render.gpu_qt import GpuQtGrid
|
|
32
|
+
from fastgrid.core.coremodel import make_model
|
|
33
|
+
page = QtWidgets.QWidget()
|
|
34
|
+
lay = QtWidgets.QVBoxLayout(page)
|
|
35
|
+
lay.setContentsMargins(0, 0, 0, 0)
|
|
36
|
+
model = make_model(headers, rows, editable=True)
|
|
37
|
+
grid = GpuQtGrid(page, model, frozen=2, col_w=col_w, scale=scale, lib=lib, **opts)
|
|
38
|
+
lay.addWidget(grid)
|
|
39
|
+
page.model, page.grid_view = model, grid # stream_styles() reads these
|
|
40
|
+
tabs.addTab(page, title)
|
|
41
|
+
choices_demo(model) # Sector/Rating dropdowns (O(1), whole-column)
|
|
42
|
+
lines_demo(model) # thick section dividers
|
|
43
|
+
readonly_demo(model) # locked Ticker + Price columns
|
|
44
|
+
model.changed() # first frame paints instantly with data + dropdowns
|
|
45
|
+
stream_styles(page) # per-cell fg/bold/bg streams in after the first frame
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
from PySide6 import QtWidgets
|
|
50
|
+
from fastgrid.core.gpu import _load_lib, _enable_dpi_awareness, _screen_scale
|
|
51
|
+
lib = _load_lib()
|
|
52
|
+
if lib is None:
|
|
53
|
+
raise SystemExit("Gpu surface unavailable -- build it with "
|
|
54
|
+
"`python -m fastgrid.core.gpu --build`.")
|
|
55
|
+
app = QtWidgets.QApplication.instance()
|
|
56
|
+
if app is None:
|
|
57
|
+
_enable_dpi_awareness()
|
|
58
|
+
app = QtWidgets.QApplication([])
|
|
59
|
+
n = rows_arg(sys.argv)
|
|
60
|
+
data = gen_rows(n) # generate once; each sheet's model copies it
|
|
61
|
+
scale = _screen_scale(None)
|
|
62
|
+
# uncapped tabs use a SMALL sheet so the empty repeatable cells are right past
|
|
63
|
+
# the data (with 100k rows you'd never scroll to the phantom region to see them).
|
|
64
|
+
small = data if n <= 30 else gen_rows(30)
|
|
65
|
+
win = QtWidgets.QTabWidget()
|
|
66
|
+
win.setWindowTitle("fastgrid (gpu-qt) — sheet options — %s rows" % f"{n:,}")
|
|
67
|
+
win.resize(round(980 * scale), round(620 * scale))
|
|
68
|
+
for title, opts in SHEETS:
|
|
69
|
+
_add_sheet(win, title, HEADERS, (small if opts else data), COL_W, scale, lib, **opts)
|
|
70
|
+
win.show()
|
|
71
|
+
app.exec()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|