nextpytk 0.4.2__tar.gz → 0.4.4__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.
- {nextpytk-0.4.2 → nextpytk-0.4.4}/.gitignore +2 -1
- {nextpytk-0.4.2 → nextpytk-0.4.4}/PKG-INFO +211 -2
- {nextpytk-0.4.2 → nextpytk-0.4.4}/README.md +210 -1
- {nextpytk-0.4.2 → nextpytk-0.4.4}/ROADMAP.md +90 -4
- {nextpytk-0.4.2 → nextpytk-0.4.4}/pyproject.toml +1 -1
- {nextpytk-0.4.2 → nextpytk-0.4.4}/src/nextpytk/app.py +550 -33
- nextpytk-0.4.4/src/nextpytk/layout.py +1691 -0
- {nextpytk-0.4.2 → nextpytk-0.4.4}/src/nextpytk/types.py +48 -0
- {nextpytk-0.4.2 → nextpytk-0.4.4}/src/nextpytk/widgets.py +1 -0
- nextpytk-0.4.2/src/nextpytk/layout.py +0 -845
- {nextpytk-0.4.2 → nextpytk-0.4.4}/LICENSE +0 -0
- {nextpytk-0.4.2 → nextpytk-0.4.4}/src/nextpytk/__init__.py +0 -0
- {nextpytk-0.4.2 → nextpytk-0.4.4}/src/nextpytk/theme.py +0 -0
- {nextpytk-0.4.2 → nextpytk-0.4.4}/src/nextpytk/tokens.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nextpytk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Accessible, declarative Tkinter applications from ordinary Python functions.
|
|
5
5
|
Project-URL: Homepage, https://github.com/nishimotz/nextpytk
|
|
6
6
|
Project-URL: Repository, https://github.com/nishimotz/nextpytk
|
|
@@ -212,6 +212,7 @@ Layout().section("msg", side=Side.LEFT, fill=Fill.X)
|
|
|
212
212
|
| `Orient` | `Orient.HORIZONTAL/VERTICAL` | scale / paned orientation |
|
|
213
213
|
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/...` | border style |
|
|
214
214
|
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
215
|
+
| `Wrap` | `Wrap.WORD/NONE/CHAR` | text wrap mode |
|
|
215
216
|
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
216
217
|
| `EventSeq` | `EventSeq.RETURN/ESCAPE/BACKSPACE/DELETE/TAB/...` | event sequences for bindings |
|
|
217
218
|
| `EventSeq` (mouse) | `EventSeq.BUTTON_1/2/3`, `DOUBLE_BUTTON_1/2/3`, `PRIMARY_DOUBLE_CLICK` | mouse event sequences |
|
|
@@ -400,6 +401,155 @@ Each name gets its own pack-based section. Extra kwargs forwarded to `section()`
|
|
|
400
401
|
Layout.from_list(["a", "b"], fill="both", expand=True)
|
|
401
402
|
```
|
|
402
403
|
|
|
404
|
+
### Layout spacing
|
|
405
|
+
|
|
406
|
+
The default block padding comes from the design token `SPACE[1]` (4 px). Pass
|
|
407
|
+
`spacing=...` to a `Layout` constructor to change the default padding for every
|
|
408
|
+
block it creates:
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
from nextpytk import tokens
|
|
412
|
+
|
|
413
|
+
layout = Layout(spacing=2).section("a").section("b").grid().widget("c").end_grid()
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
`spacing` accepts a key from `nextpytk.tokens.SPACE` (1, 2, 3, 4, 6, 8). It sets
|
|
417
|
+
the default `padx` and `pady` used by `section()`, `grid()`, `paned()`, grid
|
|
418
|
+
`widget()` placements, and nested frames. Explicit `padx`/`pady` arguments still
|
|
419
|
+
override the default.
|
|
420
|
+
|
|
421
|
+
For finer control, pass `padx` or `pady` directly to the constructor:
|
|
422
|
+
|
|
423
|
+
```python
|
|
424
|
+
Layout(padx=tokens.SPACE[4], pady=tokens.SPACE[3]).section("a")
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Nested frames
|
|
428
|
+
|
|
429
|
+
A `Layout` can be placed inside a named frame to group widgets visually without
|
|
430
|
+
mixing pack and grid in the same parent. The inner layout owns its own spacing
|
|
431
|
+
and may use any layout style (pack sections, grid, paned, or further nested
|
|
432
|
+
frames).
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
inner = Layout().section("a", "b")
|
|
436
|
+
outer = Layout().section("title").frame("group", inner).section("ok")
|
|
437
|
+
app.run(layout=outer)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
`frame(name, layout, side=..., fill=..., expand=..., padx=..., pady=...)` packs
|
|
441
|
+
the inner layout as a single block. The frame itself is registered under `name`,
|
|
442
|
+
so it can also be placed inside a grid cell:
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
outer = (
|
|
446
|
+
Layout()
|
|
447
|
+
.grid()
|
|
448
|
+
.widget("label")
|
|
449
|
+
.widget("group", sticky="nsew")
|
|
450
|
+
.end_grid()
|
|
451
|
+
.frame("group", Layout().section("a", "b"))
|
|
452
|
+
)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Paired layout
|
|
456
|
+
|
|
457
|
+
`Layout.paired(left, right, ...)` places two widgets side by side in a
|
|
458
|
+
single two-column frame. It is a lighter alternative to `app.paned` for
|
|
459
|
+
diff/compare views:
|
|
460
|
+
|
|
461
|
+
```python
|
|
462
|
+
layout = (
|
|
463
|
+
Layout()
|
|
464
|
+
.section("info")
|
|
465
|
+
.paired(
|
|
466
|
+
"left_text",
|
|
467
|
+
"right_text",
|
|
468
|
+
weight=(1, 1),
|
|
469
|
+
fill=Fill.BOTH,
|
|
470
|
+
expand=True,
|
|
471
|
+
sync_yscroll=True,
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Options:
|
|
477
|
+
|
|
478
|
+
| Argument | Default | Description |
|
|
479
|
+
|----------|---------|-------------|
|
|
480
|
+
| `weight` | `(1, 1)` | Two-column `(left, right)` grid weights |
|
|
481
|
+
| `fill` | `"x"` | Frame `fill` passed to `pack` |
|
|
482
|
+
| `expand` | `False` | Frame `expand` passed to `pack` |
|
|
483
|
+
| `sync_yscroll` | `False` | Wire the two widgets' y-scroll commands together |
|
|
484
|
+
| `line_numbers` | `False` | Add read-only line-number gutters to both sides |
|
|
485
|
+
| `side`, `padx`, `pady`, `anchor` | — | Standard `Layout` block placement options |
|
|
486
|
+
|
|
487
|
+
When `sync_yscroll=True` and both widgets are `@app.text` widgets, scrolling
|
|
488
|
+
either pane moves the other. For text widgets the same effect can also be
|
|
489
|
+
achieved per-widget with `@app.text(..., sync_yscroll_with="other")`; paired
|
|
490
|
+
layout provides a layout-level switch that works without editing widget
|
|
491
|
+
declarations.
|
|
492
|
+
|
|
493
|
+
With `line_numbers=True` each side gains a read-only line-number gutter. Both
|
|
494
|
+
panes and both gutters share a **single vertical scrollbar**, so the gutters
|
|
495
|
+
stay in lock-step with the content (no drift). Logical line numbers
|
|
496
|
+
(`1..n`) follow the pane content; the right pane's numbers update as you edit.
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
### Cluster layout
|
|
500
|
+
|
|
501
|
+
`Layout.cluster()` is a wrapping flow: widgets are not stretched to fill a
|
|
502
|
+
column; each keeps the width implied by its content or `width=` and they lay
|
|
503
|
+
out left to right, wrapping to the next row whenever the next widget no longer
|
|
504
|
+
fits in the remaining frame width. Widgets in the same row are vertically
|
|
505
|
+
centered on their midline, so a tall `entry` aligns cleanly with a shorter
|
|
506
|
+
`button` or `checkbutton`. The gap inherits the layout spacing by default.
|
|
507
|
+
|
|
508
|
+
```python
|
|
509
|
+
TAGS = ["python", "tkinter", "async", "uv", "type hints"]
|
|
510
|
+
for tag in TAGS:
|
|
511
|
+
@app.button(tag, label=f"#{tag}")
|
|
512
|
+
def on_tag(values, tag=tag):
|
|
513
|
+
return {"msg": f"Selected: {tag}"}
|
|
514
|
+
|
|
515
|
+
app.run(layout=Layout(spacing=2).status("msg").cluster(*TAGS))
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Cluster is ideal for tag clouds, toolbars, and filter UIs. The layout recomputes
|
|
519
|
+
rows automatically when the window is resized. Pass `gap=...` to override the
|
|
520
|
+
spacing default, and `side`/`fill`/`expand` to control how the cluster frame is
|
|
521
|
+
packed.
|
|
522
|
+
|
|
523
|
+
### Dynamic region switching (swap targets)
|
|
524
|
+
|
|
525
|
+
`Layout.target()` reserves a region whose contents change at runtime, mirroring
|
|
526
|
+
HTMX `hx-target` / `hx-swap`. Surrounding sections (toolbar, status) stay fixed
|
|
527
|
+
while only the target region swaps:
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
layout = Layout().cluster("go_dir", "go_file", "info").target("main_area")
|
|
531
|
+
|
|
532
|
+
@app.swap(
|
|
533
|
+
"main_area",
|
|
534
|
+
variants={
|
|
535
|
+
"dir": [Layout().section("dir_tree")],
|
|
536
|
+
"file": [Layout().paired("left_text", "right_text",
|
|
537
|
+
fill=Fill.BOTH, expand=True)],
|
|
538
|
+
},
|
|
539
|
+
default="dir",
|
|
540
|
+
)
|
|
541
|
+
def main_area():
|
|
542
|
+
pass
|
|
543
|
+
|
|
544
|
+
# Switch imperatively at runtime:
|
|
545
|
+
app.swap_view("main_area", "file")
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`@app.swap` variants are mounted up-front and shown/hidden via pack, so widget
|
|
549
|
+
state (e.g. a treeview selection or scroll position) survives switching. Each
|
|
550
|
+
variant is a `Layout` or a list of widget names. See
|
|
551
|
+
`examples/swap_demo.py`.
|
|
552
|
+
|
|
403
553
|
### Fluent DSL
|
|
404
554
|
|
|
405
555
|
**Pack sections:**
|
|
@@ -487,7 +637,8 @@ app.run(layout=b.build())
|
|
|
487
637
|
| `@app.radiobutton(name, text=..., value=..., group=..., font=...)` | ttk.Radiobutton | selected value `str` | `dict` |
|
|
488
638
|
| `@app.combobox(name, values=..., values_key=..., readonly=..., font=...)` | ttk.Combobox | selected value `str` | `dict` |
|
|
489
639
|
| `@app.menubar(name)` | tk.Menu (window menubar) | — | menu item list |
|
|
490
|
-
| `@app.
|
|
640
|
+
| `@app.filepicker(name, mode=..., label=..., title=..., filetypes=..., ...)` | ttk.Button → tkinter.filedialog | selected path(s) `str`, `list[str]`, or `None` | `dict` |
|
|
641
|
+
| `@app.text(name, width=..., height=..., font=..., wrap=..., h_scroll=...)` | tk.Text | full content `str` | `dict` |
|
|
491
642
|
| `@app.scale(name, from_=..., to=..., orient=...)` | ttk.Scale | value `str` | `dict` |
|
|
492
643
|
| `@app.spinbox(name, from_=..., to=..., values=..., font=...)` | ttk.Spinbox | value `str` | `dict` |
|
|
493
644
|
| `@app.listbox(name, items=..., items_key=..., selectmode=..., font=..., events=...)` | tk.Listbox | selected index `int` (`-1` if none) | `dict` |
|
|
@@ -495,6 +646,35 @@ app.run(layout=b.build())
|
|
|
495
646
|
|
|
496
647
|
`@app.status` sets schema / accessible `role="status"` metadata. It is **not** an ARIA live region yet (planned for a later release). Prefer it for operation feedback labels; use `@app.label` for static or high-frequency mirror text.
|
|
497
648
|
|
|
649
|
+
### File picker
|
|
650
|
+
|
|
651
|
+
`@app.filepicker` creates a button that opens a tkinter file dialog.
|
|
652
|
+
The selected path(s) are passed to the callback; `None` on cancel.
|
|
653
|
+
|
|
654
|
+
```python
|
|
655
|
+
@app.filepicker("open_file", mode="open", label="Open file",
|
|
656
|
+
title="Open file", filetypes=[("Text files", "*.txt")])
|
|
657
|
+
def pick_open_file(path: str | None) -> dict[str, Any]:
|
|
658
|
+
return {"open_path": path}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Modes: `"open"` (default), `"open_multiple"`, `"save"`, `"directory"`.
|
|
662
|
+
|
|
663
|
+
A filepicker can also be invoked from a menubar item by using its name
|
|
664
|
+
as the `command`:
|
|
665
|
+
|
|
666
|
+
```python
|
|
667
|
+
@app.filepicker("m_open", mode="open", title="Open file")
|
|
668
|
+
def m_open(path):
|
|
669
|
+
return {"open_path": path}
|
|
670
|
+
|
|
671
|
+
@app.menubar("menu")
|
|
672
|
+
def menu_bar():
|
|
673
|
+
return [{"label": "File", "items": [
|
|
674
|
+
{"label": "Open...", "command": "m_open"},
|
|
675
|
+
]}]
|
|
676
|
+
```
|
|
677
|
+
|
|
498
678
|
`app.run(stages=..., tabposition=...)` and `@app.stages` provide state-driven screen switching (one visible stage at a time). Theme helpers (`apply_theme`, `tokens`, layout chrome) ship in the package root — see `examples/header_demo.py`.
|
|
499
679
|
|
|
500
680
|
Common options:
|
|
@@ -509,6 +689,32 @@ Entry options: `placeholder`, `show`, `font`, `padding`, `width`, `state`.
|
|
|
509
689
|
Button, checkbutton, radiobutton, spinbox, combobox, listbox, text options:
|
|
510
690
|
- `font` is accepted uniformly. Under the hood, ttk widgets use a derived style so the rest of the theme (colors, maps, layout) is inherited.
|
|
511
691
|
|
|
692
|
+
Text options (continued):
|
|
693
|
+
- `wrap`: `Wrap.WORD` (default) / `Wrap.NONE` / `Wrap.CHAR`. `Wrap.NONE` keeps each logical line on a single row.
|
|
694
|
+
- `h_scroll`: when `True`, a horizontal scrollbar is added (and wired to `xscrollcommand`) so long lines are reachable, typically used with `wrap=Wrap.NONE`.
|
|
695
|
+
|
|
696
|
+
Every widget decorator also accepts `widget_kwargs: dict` for per-widget
|
|
697
|
+
design-token overrides applied after construction. Keys are widget-native
|
|
698
|
+
tk/ttk options (`padx`, `pady`, `bg`, `fg`, `font`, …). An invalid/unknown
|
|
699
|
+
key is ignored rather than aborting the build:
|
|
700
|
+
|
|
701
|
+
```python
|
|
702
|
+
@app.text("log", wrap="none", h_scroll=True,
|
|
703
|
+
widget_kwargs={"bg": "#1e1e1e", "fg": "#dcdcdc"})
|
|
704
|
+
def log(value): return {}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
Runtime access: `app.text_widget(name)` returns the real `tk.Text`;
|
|
708
|
+
`app.layout_frame(name)` returns the layout section frame that owns a
|
|
709
|
+
widget (for manual grid/pack placement or re-parenting).
|
|
710
|
+
`app.on_text_set(name, hook)` registers a callback to run after a text
|
|
711
|
+
widget's content is replaced (used by paired line-number gutters).
|
|
712
|
+
|
|
713
|
+
Enum-like options (`wrap`, `state`, `orient`, `selectmode`, `mode`) are
|
|
714
|
+
validated at registration time: an invalid value raises a clear `ValueError`
|
|
715
|
+
naming the option and the allowed values, instead of failing later with a
|
|
716
|
+
`TclError` when the widget is built.
|
|
717
|
+
|
|
512
718
|
`@app.message` creates an auto-wrapping label. `width` sets initial pixel width; `auto_width=True` (default) tracks parent container resize.
|
|
513
719
|
|
|
514
720
|
---
|
|
@@ -596,7 +802,10 @@ uv run python examples/widget_gallery.py # all widget types
|
|
|
596
802
|
uv run python examples/header_demo.py # Layout.header / .status chrome
|
|
597
803
|
uv run python examples/combobox_demo.py # ttk.Combobox
|
|
598
804
|
uv run python examples/menubar_demo.py # menubar
|
|
805
|
+
uv run python examples/filepicker_demo.py # file picker
|
|
599
806
|
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
807
|
+
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
808
|
+
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
600
809
|
```
|
|
601
810
|
|
|
602
811
|
---
|
|
@@ -188,6 +188,7 @@ Layout().section("msg", side=Side.LEFT, fill=Fill.X)
|
|
|
188
188
|
| `Orient` | `Orient.HORIZONTAL/VERTICAL` | scale / paned orientation |
|
|
189
189
|
| `Relief` | `Relief.FLAT/RAISED/SUNKEN/...` | border style |
|
|
190
190
|
| `Justify` | `Justify.LEFT/RIGHT/CENTER` | text alignment |
|
|
191
|
+
| `Wrap` | `Wrap.WORD/NONE/CHAR` | text wrap mode |
|
|
191
192
|
| `SelectMode` | `SelectMode.SINGLE/BROWSE/MULTIPLE/EXTENDED` | listbox mode |
|
|
192
193
|
| `EventSeq` | `EventSeq.RETURN/ESCAPE/BACKSPACE/DELETE/TAB/...` | event sequences for bindings |
|
|
193
194
|
| `EventSeq` (mouse) | `EventSeq.BUTTON_1/2/3`, `DOUBLE_BUTTON_1/2/3`, `PRIMARY_DOUBLE_CLICK` | mouse event sequences |
|
|
@@ -376,6 +377,155 @@ Each name gets its own pack-based section. Extra kwargs forwarded to `section()`
|
|
|
376
377
|
Layout.from_list(["a", "b"], fill="both", expand=True)
|
|
377
378
|
```
|
|
378
379
|
|
|
380
|
+
### Layout spacing
|
|
381
|
+
|
|
382
|
+
The default block padding comes from the design token `SPACE[1]` (4 px). Pass
|
|
383
|
+
`spacing=...` to a `Layout` constructor to change the default padding for every
|
|
384
|
+
block it creates:
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
from nextpytk import tokens
|
|
388
|
+
|
|
389
|
+
layout = Layout(spacing=2).section("a").section("b").grid().widget("c").end_grid()
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
`spacing` accepts a key from `nextpytk.tokens.SPACE` (1, 2, 3, 4, 6, 8). It sets
|
|
393
|
+
the default `padx` and `pady` used by `section()`, `grid()`, `paned()`, grid
|
|
394
|
+
`widget()` placements, and nested frames. Explicit `padx`/`pady` arguments still
|
|
395
|
+
override the default.
|
|
396
|
+
|
|
397
|
+
For finer control, pass `padx` or `pady` directly to the constructor:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
Layout(padx=tokens.SPACE[4], pady=tokens.SPACE[3]).section("a")
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Nested frames
|
|
404
|
+
|
|
405
|
+
A `Layout` can be placed inside a named frame to group widgets visually without
|
|
406
|
+
mixing pack and grid in the same parent. The inner layout owns its own spacing
|
|
407
|
+
and may use any layout style (pack sections, grid, paned, or further nested
|
|
408
|
+
frames).
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
inner = Layout().section("a", "b")
|
|
412
|
+
outer = Layout().section("title").frame("group", inner).section("ok")
|
|
413
|
+
app.run(layout=outer)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
`frame(name, layout, side=..., fill=..., expand=..., padx=..., pady=...)` packs
|
|
417
|
+
the inner layout as a single block. The frame itself is registered under `name`,
|
|
418
|
+
so it can also be placed inside a grid cell:
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
outer = (
|
|
422
|
+
Layout()
|
|
423
|
+
.grid()
|
|
424
|
+
.widget("label")
|
|
425
|
+
.widget("group", sticky="nsew")
|
|
426
|
+
.end_grid()
|
|
427
|
+
.frame("group", Layout().section("a", "b"))
|
|
428
|
+
)
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Paired layout
|
|
432
|
+
|
|
433
|
+
`Layout.paired(left, right, ...)` places two widgets side by side in a
|
|
434
|
+
single two-column frame. It is a lighter alternative to `app.paned` for
|
|
435
|
+
diff/compare views:
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
layout = (
|
|
439
|
+
Layout()
|
|
440
|
+
.section("info")
|
|
441
|
+
.paired(
|
|
442
|
+
"left_text",
|
|
443
|
+
"right_text",
|
|
444
|
+
weight=(1, 1),
|
|
445
|
+
fill=Fill.BOTH,
|
|
446
|
+
expand=True,
|
|
447
|
+
sync_yscroll=True,
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Options:
|
|
453
|
+
|
|
454
|
+
| Argument | Default | Description |
|
|
455
|
+
|----------|---------|-------------|
|
|
456
|
+
| `weight` | `(1, 1)` | Two-column `(left, right)` grid weights |
|
|
457
|
+
| `fill` | `"x"` | Frame `fill` passed to `pack` |
|
|
458
|
+
| `expand` | `False` | Frame `expand` passed to `pack` |
|
|
459
|
+
| `sync_yscroll` | `False` | Wire the two widgets' y-scroll commands together |
|
|
460
|
+
| `line_numbers` | `False` | Add read-only line-number gutters to both sides |
|
|
461
|
+
| `side`, `padx`, `pady`, `anchor` | — | Standard `Layout` block placement options |
|
|
462
|
+
|
|
463
|
+
When `sync_yscroll=True` and both widgets are `@app.text` widgets, scrolling
|
|
464
|
+
either pane moves the other. For text widgets the same effect can also be
|
|
465
|
+
achieved per-widget with `@app.text(..., sync_yscroll_with="other")`; paired
|
|
466
|
+
layout provides a layout-level switch that works without editing widget
|
|
467
|
+
declarations.
|
|
468
|
+
|
|
469
|
+
With `line_numbers=True` each side gains a read-only line-number gutter. Both
|
|
470
|
+
panes and both gutters share a **single vertical scrollbar**, so the gutters
|
|
471
|
+
stay in lock-step with the content (no drift). Logical line numbers
|
|
472
|
+
(`1..n`) follow the pane content; the right pane's numbers update as you edit.
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
### Cluster layout
|
|
476
|
+
|
|
477
|
+
`Layout.cluster()` is a wrapping flow: widgets are not stretched to fill a
|
|
478
|
+
column; each keeps the width implied by its content or `width=` and they lay
|
|
479
|
+
out left to right, wrapping to the next row whenever the next widget no longer
|
|
480
|
+
fits in the remaining frame width. Widgets in the same row are vertically
|
|
481
|
+
centered on their midline, so a tall `entry` aligns cleanly with a shorter
|
|
482
|
+
`button` or `checkbutton`. The gap inherits the layout spacing by default.
|
|
483
|
+
|
|
484
|
+
```python
|
|
485
|
+
TAGS = ["python", "tkinter", "async", "uv", "type hints"]
|
|
486
|
+
for tag in TAGS:
|
|
487
|
+
@app.button(tag, label=f"#{tag}")
|
|
488
|
+
def on_tag(values, tag=tag):
|
|
489
|
+
return {"msg": f"Selected: {tag}"}
|
|
490
|
+
|
|
491
|
+
app.run(layout=Layout(spacing=2).status("msg").cluster(*TAGS))
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Cluster is ideal for tag clouds, toolbars, and filter UIs. The layout recomputes
|
|
495
|
+
rows automatically when the window is resized. Pass `gap=...` to override the
|
|
496
|
+
spacing default, and `side`/`fill`/`expand` to control how the cluster frame is
|
|
497
|
+
packed.
|
|
498
|
+
|
|
499
|
+
### Dynamic region switching (swap targets)
|
|
500
|
+
|
|
501
|
+
`Layout.target()` reserves a region whose contents change at runtime, mirroring
|
|
502
|
+
HTMX `hx-target` / `hx-swap`. Surrounding sections (toolbar, status) stay fixed
|
|
503
|
+
while only the target region swaps:
|
|
504
|
+
|
|
505
|
+
```python
|
|
506
|
+
layout = Layout().cluster("go_dir", "go_file", "info").target("main_area")
|
|
507
|
+
|
|
508
|
+
@app.swap(
|
|
509
|
+
"main_area",
|
|
510
|
+
variants={
|
|
511
|
+
"dir": [Layout().section("dir_tree")],
|
|
512
|
+
"file": [Layout().paired("left_text", "right_text",
|
|
513
|
+
fill=Fill.BOTH, expand=True)],
|
|
514
|
+
},
|
|
515
|
+
default="dir",
|
|
516
|
+
)
|
|
517
|
+
def main_area():
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
# Switch imperatively at runtime:
|
|
521
|
+
app.swap_view("main_area", "file")
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
`@app.swap` variants are mounted up-front and shown/hidden via pack, so widget
|
|
525
|
+
state (e.g. a treeview selection or scroll position) survives switching. Each
|
|
526
|
+
variant is a `Layout` or a list of widget names. See
|
|
527
|
+
`examples/swap_demo.py`.
|
|
528
|
+
|
|
379
529
|
### Fluent DSL
|
|
380
530
|
|
|
381
531
|
**Pack sections:**
|
|
@@ -463,7 +613,8 @@ app.run(layout=b.build())
|
|
|
463
613
|
| `@app.radiobutton(name, text=..., value=..., group=..., font=...)` | ttk.Radiobutton | selected value `str` | `dict` |
|
|
464
614
|
| `@app.combobox(name, values=..., values_key=..., readonly=..., font=...)` | ttk.Combobox | selected value `str` | `dict` |
|
|
465
615
|
| `@app.menubar(name)` | tk.Menu (window menubar) | — | menu item list |
|
|
466
|
-
| `@app.
|
|
616
|
+
| `@app.filepicker(name, mode=..., label=..., title=..., filetypes=..., ...)` | ttk.Button → tkinter.filedialog | selected path(s) `str`, `list[str]`, or `None` | `dict` |
|
|
617
|
+
| `@app.text(name, width=..., height=..., font=..., wrap=..., h_scroll=...)` | tk.Text | full content `str` | `dict` |
|
|
467
618
|
| `@app.scale(name, from_=..., to=..., orient=...)` | ttk.Scale | value `str` | `dict` |
|
|
468
619
|
| `@app.spinbox(name, from_=..., to=..., values=..., font=...)` | ttk.Spinbox | value `str` | `dict` |
|
|
469
620
|
| `@app.listbox(name, items=..., items_key=..., selectmode=..., font=..., events=...)` | tk.Listbox | selected index `int` (`-1` if none) | `dict` |
|
|
@@ -471,6 +622,35 @@ app.run(layout=b.build())
|
|
|
471
622
|
|
|
472
623
|
`@app.status` sets schema / accessible `role="status"` metadata. It is **not** an ARIA live region yet (planned for a later release). Prefer it for operation feedback labels; use `@app.label` for static or high-frequency mirror text.
|
|
473
624
|
|
|
625
|
+
### File picker
|
|
626
|
+
|
|
627
|
+
`@app.filepicker` creates a button that opens a tkinter file dialog.
|
|
628
|
+
The selected path(s) are passed to the callback; `None` on cancel.
|
|
629
|
+
|
|
630
|
+
```python
|
|
631
|
+
@app.filepicker("open_file", mode="open", label="Open file",
|
|
632
|
+
title="Open file", filetypes=[("Text files", "*.txt")])
|
|
633
|
+
def pick_open_file(path: str | None) -> dict[str, Any]:
|
|
634
|
+
return {"open_path": path}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Modes: `"open"` (default), `"open_multiple"`, `"save"`, `"directory"`.
|
|
638
|
+
|
|
639
|
+
A filepicker can also be invoked from a menubar item by using its name
|
|
640
|
+
as the `command`:
|
|
641
|
+
|
|
642
|
+
```python
|
|
643
|
+
@app.filepicker("m_open", mode="open", title="Open file")
|
|
644
|
+
def m_open(path):
|
|
645
|
+
return {"open_path": path}
|
|
646
|
+
|
|
647
|
+
@app.menubar("menu")
|
|
648
|
+
def menu_bar():
|
|
649
|
+
return [{"label": "File", "items": [
|
|
650
|
+
{"label": "Open...", "command": "m_open"},
|
|
651
|
+
]}]
|
|
652
|
+
```
|
|
653
|
+
|
|
474
654
|
`app.run(stages=..., tabposition=...)` and `@app.stages` provide state-driven screen switching (one visible stage at a time). Theme helpers (`apply_theme`, `tokens`, layout chrome) ship in the package root — see `examples/header_demo.py`.
|
|
475
655
|
|
|
476
656
|
Common options:
|
|
@@ -485,6 +665,32 @@ Entry options: `placeholder`, `show`, `font`, `padding`, `width`, `state`.
|
|
|
485
665
|
Button, checkbutton, radiobutton, spinbox, combobox, listbox, text options:
|
|
486
666
|
- `font` is accepted uniformly. Under the hood, ttk widgets use a derived style so the rest of the theme (colors, maps, layout) is inherited.
|
|
487
667
|
|
|
668
|
+
Text options (continued):
|
|
669
|
+
- `wrap`: `Wrap.WORD` (default) / `Wrap.NONE` / `Wrap.CHAR`. `Wrap.NONE` keeps each logical line on a single row.
|
|
670
|
+
- `h_scroll`: when `True`, a horizontal scrollbar is added (and wired to `xscrollcommand`) so long lines are reachable, typically used with `wrap=Wrap.NONE`.
|
|
671
|
+
|
|
672
|
+
Every widget decorator also accepts `widget_kwargs: dict` for per-widget
|
|
673
|
+
design-token overrides applied after construction. Keys are widget-native
|
|
674
|
+
tk/ttk options (`padx`, `pady`, `bg`, `fg`, `font`, …). An invalid/unknown
|
|
675
|
+
key is ignored rather than aborting the build:
|
|
676
|
+
|
|
677
|
+
```python
|
|
678
|
+
@app.text("log", wrap="none", h_scroll=True,
|
|
679
|
+
widget_kwargs={"bg": "#1e1e1e", "fg": "#dcdcdc"})
|
|
680
|
+
def log(value): return {}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Runtime access: `app.text_widget(name)` returns the real `tk.Text`;
|
|
684
|
+
`app.layout_frame(name)` returns the layout section frame that owns a
|
|
685
|
+
widget (for manual grid/pack placement or re-parenting).
|
|
686
|
+
`app.on_text_set(name, hook)` registers a callback to run after a text
|
|
687
|
+
widget's content is replaced (used by paired line-number gutters).
|
|
688
|
+
|
|
689
|
+
Enum-like options (`wrap`, `state`, `orient`, `selectmode`, `mode`) are
|
|
690
|
+
validated at registration time: an invalid value raises a clear `ValueError`
|
|
691
|
+
naming the option and the allowed values, instead of failing later with a
|
|
692
|
+
`TclError` when the widget is built.
|
|
693
|
+
|
|
488
694
|
`@app.message` creates an auto-wrapping label. `width` sets initial pixel width; `auto_width=True` (default) tracks parent container resize.
|
|
489
695
|
|
|
490
696
|
---
|
|
@@ -572,7 +778,10 @@ uv run python examples/widget_gallery.py # all widget types
|
|
|
572
778
|
uv run python examples/header_demo.py # Layout.header / .status chrome
|
|
573
779
|
uv run python examples/combobox_demo.py # ttk.Combobox
|
|
574
780
|
uv run python examples/menubar_demo.py # menubar
|
|
781
|
+
uv run python examples/filepicker_demo.py # file picker
|
|
575
782
|
uv run python examples/disk_usage_flat_async.py # ncdu-style viewer (async)
|
|
783
|
+
uv run python examples/paired_demo.py # side-by-side paired layout with y-scroll sync
|
|
784
|
+
uv run python examples/swap_demo.py # dynamic region switching (Layout.target + @app.swap)
|
|
576
785
|
```
|
|
577
786
|
|
|
578
787
|
---
|
|
@@ -130,7 +130,87 @@ PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
133
|
-
##
|
|
133
|
+
## Snapshot (v0.4.3) — filepicker + layout blocks
|
|
134
|
+
|
|
135
|
+
- Added `@app.filepicker` (`filedialog` wrapper; `mode=` open/save/directory/open_multiple).
|
|
136
|
+
- Added `Layout(spacing=...)` for a layout-wide SPACE token default (padx/pady inheritance).
|
|
137
|
+
- Added `Layout.frame(...)` for nested layouts (`Layout` inside `Layout`).
|
|
138
|
+
- Added `Layout.cluster(...)` wrapping flow: widgets keep content/`width=` sizes and wrap left-to-right.
|
|
139
|
+
- Added `Layout.paired(left, right, ..., sync_yscroll=)` for side-by-side panes with optional text y-scroll sync.
|
|
140
|
+
- Docs/examples/tests for the above (`filepicker_demo`, `nested_frame_demo`, `cluster_demo`, `paired_demo`).
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Snapshot (v0.4.4) — text wrap/h-scroll, public runtime API
|
|
145
|
+
|
|
146
|
+
### Text widget ergonomics (continued)
|
|
147
|
+
|
|
148
|
+
- Added `wrap=` option to `@app.text` for logical-line display control:
|
|
149
|
+
- `Wrap.WORD` (default) — wrap at word boundaries.
|
|
150
|
+
- `Wrap.NONE` — keep each logical line on one row (requires horizontal scrolling).
|
|
151
|
+
- `Wrap.CHAR` — wrap at any character boundary.
|
|
152
|
+
- Added a new `nextpytk.types.Wrap` typed-constant class and `WrapLike` union.
|
|
153
|
+
- Added `h_scroll=` option to `@app.text`: when True, a horizontal scrollbar is added (and wired to `xscrollcommand`) so long lines are reachable in `Wrap.NONE` mode. The horizontal scrollbar is tracked in `app._text_hscrollbars`.
|
|
154
|
+
|
|
155
|
+
### Decorator type-definition sync
|
|
156
|
+
|
|
157
|
+
- `TreeviewOptions.double_click` is now typed and honored by `@app.treeview`.
|
|
158
|
+
- Defaults to `True`: the `activate` handler fires on double-click (previous behavior).
|
|
159
|
+
- When `False`, `activate` fires on a single `ButtonRelease-1` instead.
|
|
160
|
+
- `_on_treeview_activate` accepts an optional `event` argument so the single-click binding can reuse it.
|
|
161
|
+
|
|
162
|
+
### Public runtime API
|
|
163
|
+
|
|
164
|
+
- Added `app.layout_frame(name)` — returns the layout frame that owns a widget (the section frame), so applications can place/re-parent widgets with grid/pack without reaching into the private `_widget_masters`.
|
|
165
|
+
- This is the public replacement for the internal `_widget_masters` access that previously forced applications to depend on private state.
|
|
166
|
+
|
|
167
|
+
### Per-widget design-token overrides
|
|
168
|
+
|
|
169
|
+
- All widget decorators now accept `widget_kwargs: dict[str, Any]` (on `CommonWidgetOptions`).
|
|
170
|
+
- Keys are widget-native tk/ttk options (`padx`, `pady`, `bg`, `fg`, `font`, …); values are the native values.
|
|
171
|
+
- Overrides are applied after construction by `_apply_widget_overrides()`. An invalid/unknown key raises no error: a `TclError` is swallowed so one bad key does not abort the build.
|
|
172
|
+
- This removes the need to monkey-patch the shared `tokens` module to tweak a single widget's appearance.
|
|
173
|
+
|
|
174
|
+
### Decorator argument validation
|
|
175
|
+
|
|
176
|
+
- Added `_validate_choice()` / `_validate_positive_int()` helpers used by the decorators to reject invalid enum-like and non-positive-int options at **registration time** (before any Tk widget is built).
|
|
177
|
+
- Validated options:
|
|
178
|
+
- `text`: `state` (`normal`/`disabled`/`active`), `wrap` (`word`/`none`/`char`)
|
|
179
|
+
- `scale`, `progressbar`, `paned`: `orient` (`horizontal`/`vertical`)
|
|
180
|
+
- `listbox`, `treeview`: `selectmode` (`single`/`browse`/`multiple`/`extended`)
|
|
181
|
+
- `filepicker`: `mode` (`open`/`open_multiple`/`save`/`directory`)
|
|
182
|
+
- `progressbar`: `mode` (`determinate`/`indeterminate`)
|
|
183
|
+
- A mistyped value (e.g. `wrap="wrap"`) now raises a clear `ValueError` naming the option, widget, and allowed values — instead of a cryptic `_tkinter.TclError` at runtime.
|
|
184
|
+
|
|
185
|
+
### Paired line-number gutters
|
|
186
|
+
|
|
187
|
+
- Added `line_numbers=True` to `Layout.paired(...)`.
|
|
188
|
+
- Each side gains a read-only line-number gutter (`disabled` tk.Text, `takefocus=0`, arrow cursor).
|
|
189
|
+
- **Reliable sync via a single shared vertical scrollbar**: both panes and both gutters drive one shared scrollbar. Each widget's `yscrollcommand` is chained so scrolling any widget moves the other three, and the shared scrollbar's `command` drives all four. This avoids the `yview_moveto` drift that plagued per-widget gutter chaining.
|
|
190
|
+
- Gutters stay in sync when content changes via `app.text_set()` (a new `app.on_text_set()` hook) and when the editable right pane is typed into (`<KeyRelease>`).
|
|
191
|
+
- Added `examples/paired_demo.py` demonstrating `line_numbers=True`.
|
|
192
|
+
- The new `app.on_text_set(name, hook)` public helper lets layout features react to programmatic text replacement.
|
|
193
|
+
|
|
194
|
+
### Dynamic layout switching (swap targets)
|
|
195
|
+
|
|
196
|
+
- Added `Layout.target(name)` to reserve a swap region whose contents change at runtime (mirrors HTMX `hx-target`).
|
|
197
|
+
- Added `@app.swap(name, variants=..., default=...)` to declare the layouts that can fill a target (mirrors HTMX `hx-swap`), and `app.swap_view(name, variant)` to switch imperatively.
|
|
198
|
+
- Variants are mounted up-front and shown/hidden via `pack`/`pack_forget`, so widget state (treeview selection, scroll position) survives switching. Surrounding `Layout.section` blocks (toolbar, status) stay fixed.
|
|
199
|
+
- Added `examples/swap_demo.py` (folder view <-> paired diff view).
|
|
200
|
+
|
|
201
|
+
### Tests
|
|
202
|
+
|
|
203
|
+
- Added `tests/test_widget_public_api.py` covering `layout_frame` and `widget_kwargs` (applied, ignored-invalid).
|
|
204
|
+
- Added `tests/test_decorator_validation.py` covering invalid-option rejection for every validated widget.
|
|
205
|
+
- Added wrap/h-scroll tests to `tests/test_text.py`.
|
|
206
|
+
- Added `double_click` tests to `tests/test_state.py`.
|
|
207
|
+
- Added paired line-number gutter tests to `tests/test_paired.py` (creation, population, shared-scrollbar sync).
|
|
208
|
+
- Added swap tests to `tests/test_swap.py` (variant registration, build, switching, back-and-forth, pre-build intent).
|
|
209
|
+
- Suite grew from 181 to 208 passing; pyright clean.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Near term (post-0.4.4)
|
|
134
214
|
|
|
135
215
|
### A11y
|
|
136
216
|
|
|
@@ -147,21 +227,27 @@ PyPI: Trusted Publishing on `v*` tag push (`.github/workflows/publish.yml`).
|
|
|
147
227
|
### Widget expansion
|
|
148
228
|
|
|
149
229
|
- [x] Per-widget `bind` for entry and listbox (`events=` on `@app.entry` and `@app.listbox`)
|
|
230
|
+
- [x] Per-widget `bind` for other widgets via `widget_kwargs` / widget-level options
|
|
150
231
|
- [ ] Per-widget `bind` for other widgets (`text`, `combobox`, `treeview`, `button`, etc.)
|
|
151
232
|
- [x] listbox callback value design: keep selection index (unify with `treeview`)
|
|
233
|
+
- [x] `@app.filepicker` — `filedialog` wrapper (schema/tool naming still open)
|
|
234
|
+
- [x] Text `wrap=` / `h_scroll=` options (v0.4.4)
|
|
152
235
|
|
|
153
236
|
### Layout DSL
|
|
154
237
|
|
|
155
238
|
- [x] Deprecate index-order plural methods `col_weights`/`row_weights`/`col_minsizes`/`row_minsizes` in 0.4.1 (remove in 0.5.0).
|
|
156
239
|
- [ ] CSS-grid-inspired layout API for 0.5.0: explicit `grid-template-columns` / `grid-template-rows` strings (e.g. `"1fr 2fr"`, `"auto 1fr"`), area-based placement, and gap tokens. Reduce misuse by making rows/columns and widget placement visually aligned in one declaration.
|
|
157
|
-
- [
|
|
158
|
-
- [
|
|
240
|
+
- [x] Nested frames (`Layout` inside `Layout`) via `Layout.frame(...)`
|
|
241
|
+
- [x] Global default for `padx`/`pady` via `Layout(spacing=...)`
|
|
242
|
+
- [x] `Layout.cluster(...)` wrapping flow; `Layout.paired(..., sync_yscroll=)`
|
|
243
|
+
- [x] `Layout.paired(..., line_numbers=True)` read-only line-number gutters (v0.4.4)
|
|
244
|
+
- [x] `Layout.target(...)` + `@app.swap(...)` dynamic region switching (v0.4.4)
|
|
159
245
|
|
|
160
246
|
### Agent / LLM integration
|
|
161
247
|
|
|
162
248
|
- [ ] Expose `schema()` as Function Calling definition (bind `sequence`, treeview column defs, etc.)
|
|
163
249
|
- [ ] `@agent_tool` integration (GUI operations as agent vocabulary)
|
|
164
|
-
- [ ] `@app.filepicker`
|
|
250
|
+
- [ ] Surface `@app.filepicker` in `schema()` / tool vocabulary
|
|
165
251
|
|
|
166
252
|
---
|
|
167
253
|
|