Skip to content

Commit aeeff59

Browse files
v0.4.0: add log-view% (scrolling log/console) + stretchability footgun
log-view% — editor-canvas% + text% wrapped as a scrolling, read-only, monospace log that stretches to fill its parent (fixing the 'height won't stretch' trap), auto-scrolls on append, accepts programmatic appends while read-only to the user, and trims old lines past max-lines. Includes a flashing-tool-style demo and is added to the showcase. New footgun in AGENTS.md: a widget only resizes with the window if it is stretchable AND its parent stretches children (pane% / group-panel% do not). Verified: 15 examples launch, 21 tests pass, raco make + scribble clean; append/trim/clear behavior checked end-to-end.
1 parent 0af5082 commit aeeff59

11 files changed

Lines changed: 171 additions & 3 deletions

File tree

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ A curated collection of GUI widgets for Racket. Two rules:
6464
| Display an image (fit or fixed scale) | `image-view%` | `(new image-view% [parent f] [bitmap bmp] [scale 'fit])` or `(send iv load-file "x.png")` |
6565
| Bottom status bar (+ optional progress) | `status-bar%` | `(new status-bar% [parent f] [show-progress #t] [initial-message "Ready."])` |
6666
| Modal "Working… / Cancel" dialog | `progress-dialog%` | `(new progress-dialog% [parent f] [label "..."])`; drive from a thread + `queue-callback`, see `examples/progress-dialog-demo.rkt` |
67+
| Scrolling log / console output | `log-view%` | `(new log-view% [parent f] [max-lines 5000])`; `(send log append-line "...")` auto-scrolls |
6768
| "Busy", unknown duration | `spinner%` | `(new spinner% [parent f] [diameter 28])` then `(send sp start)` / `(send sp stop)` |
6869
| Compact `[-] value [+]` numeric | `stepper%` | `(new stepper% [parent f] [min-value 0] [max-value 20] [initial 5])` |
6970
| Collapsible ("Advanced…") section | `disclosure%` | `(new disclosure% [parent f] [label "Advanced"] [expanded? #f])` — add children to `(send d get-content)` |
@@ -96,6 +97,13 @@ otherwise copy:
9697
- **`message%` grows to fit its label.** For a status bar use `min-width` +
9798
`stretchable-width #t` so long text is clipped instead of stretching the
9899
window.
100+
- **A widget only resizes with the window if it is stretchable AND its parent
101+
stretches children.** `editor-canvas%`, `canvas%` and `panel%` default to
102+
stretchable, but `pane%` and `group-panel%` do NOT stretch their children
103+
(they use natural size) — a resizing area placed in one will not grow. Put it
104+
in a `horizontal-panel%`/`vertical-panel%`, or set
105+
`(send w stretchable-width/height #t)`. `log-view%` and `image-view%`
106+
stretch by default.
99107
- **`tab-panel%` tabs are visual only.** There is **no per-instance selection
100108
callback**, and `on-new-tab` is **not augmentable**. To switch pages, drive it
101109
from a `choice%`, or augment `on-new-request`. The tabs do not hide/show

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ All notable changes to widgetkit are documented here.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [0.4.0] — 2026-08-13
8+
9+
### Added — gap-filling widgets
10+
- `log-view%` — a scrolling, read-only, monospace log/console that stretches to
11+
fill its parent, auto-scrolls on append, and trims old lines past a cap.
12+
Directly addresses the "build a flashing-tool log panel" use case that is
13+
painful to assemble from raw `editor-canvas%` + `text%`.
14+
15+
### Added — footguns
16+
- Stretchability rule: a widget only resizes with the window if it is
17+
stretchable AND its parent stretches children (`pane%` and `group-panel%` do
18+
not stretch children — use a `panel%`/`horizontal-panel%`/`vertical-panel%`).
19+
720
## [0.3.0] — 2026-08-13
821

922
### Added — gap-filling widgets

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ racket examples/showcase.rkt
6666
| `disclosure%` | no collapsible / expand-collapse container (the "Advanced options…" pattern) |
6767
| `image-view%` | `canvas%` exists, but no ready-made widget to just display an image (fit / fixed scale) |
6868
| `progress-dialog%` | no modal "Working… / Cancel" dialog for long tasks (core has `gauge%` only) |
69+
| `log-view%` | no scrolling, read-only, auto-scroll log/console; raw `editor-canvas%` + `text%` traps users on stretch & auto-scroll |
6970

7071
### Convenience wrappers (new, MIT)
7172

@@ -105,6 +106,7 @@ Every widget has a minimal, standalone runnable example in [`examples/`](example
105106
| `disclosure-demo.rkt` | `disclosure%` (collapsible section) |
106107
| `image-view-demo.rkt` | `image-view%` (display a bitmap) |
107108
| `progress-dialog-demo.rkt` | `progress-dialog%` (modal progress + cancel) |
109+
| `log-view-demo.rkt` | `log-view%` (scrolling log, flashing-tool style) |
108110

109111
```bash
110112
racket examples/status-bar-demo.rkt # any of them

README.zh-CN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ racket examples/showcase.rkt
6666
| `disclosure%` | 没有可折叠/展开收起的容器("高级选项…"那种) |
6767
| `image-view%` |`canvas%`,但没有现成控件直接显示一张图片(fit / 固定缩放) |
6868
| `progress-dialog%` | 没有长任务用的模态"处理中… / 取消"对话框(core 只有 `gauge%`|
69+
| `log-view%` | 没有可滚动、只读、自动滚到底的日志/控制台;手搓 `editor-canvas%`+`text%` 卡在伸缩与自动滚动 |
6970

7071
### 便捷包装(新写,MIT)
7172

@@ -105,6 +106,7 @@ racket examples/showcase.rkt
105106
| `disclosure-demo.rkt` | `disclosure%`(可折叠分组) |
106107
| `image-view-demo.rkt` | `image-view%`(显示图片) |
107108
| `progress-dialog-demo.rkt` | `progress-dialog%`(模态进度 + 取消) |
109+
| `log-view-demo.rkt` | `log-view%`(滚动日志,刷写工具风格) |
108110

109111
```bash
110112
racket examples/status-bar-demo.rkt # 任选一个

examples/log-view-demo.rkt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
;; log-view% — minimal demo (scrolling log, flashing-tool style)
2+
;; Run: racket examples/log-view-demo.rkt
3+
#lang racket/base
4+
5+
(require racket/class
6+
racket/gui/base
7+
widgetkit)
8+
9+
;; The frame stacks a fixed-height button row on top of a log-view% that
10+
;; stretches to fill the remaining vertical space — resizing the window grows
11+
;; the log, which is exactly what was painful to get right by hand.
12+
(define f (new frame% [label "log-view% demo"] [width 620] [height 400]
13+
[alignment '(left top)]))
14+
15+
(define buttons (new horizontal-panel% [parent f] [alignment '(left center)]
16+
[stretchable-height #f] [spacing 8]))
17+
18+
(define log (new log-view% [parent f] [min-height 200]))
19+
20+
(new button% [parent buttons] [label "Append log line"]
21+
[callback (λ (_b _e)
22+
(send log append-line
23+
(format "[~a ms] erasing sector 0x~x ... ok"
24+
(current-inexact-milliseconds) (random 4096))))])
25+
(new button% [parent buttons] [label "Append 500 lines (stress)"]
26+
[callback (λ (_b _e)
27+
(for ([i (in-range 500)])
28+
(send log append-line (format "progress ~a / 500" i))))])
29+
(new button% [parent buttons] [label "Clear"]
30+
[callback (λ (_b _e) (send log clear))])
31+
32+
(send log append-line "Bootloader v2.3 — ready.")
33+
(send log append-line "Press 'Append log line' to stream output; the view auto-scrolls.")
34+
35+
(send f show #t)

examples/showcase.rkt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
(send bdc draw-text "widgetkit" 16 40)
8181
(send bdc set-bitmap #f)
8282
(new image-view% [parent p-feedback] [bitmap bmp] [scale 'fit] [min-height 120])
83+
(section p-feedback "log-view% — scrolling log (auto-scrolls on append)")
84+
(define showcase-log (new log-view% [parent p-feedback] [min-height 100]))
85+
(new button% [parent p-feedback] [label "Append a log line"]
86+
[callback (λ (_b _e) (send showcase-log append-line (format "event @ ~a ms" (current-inexact-milliseconds))))])
8387
(section p-feedback "status-bar% — see the bottom of this window (with progress gauge)")
8488

8589
;; --- Status bar (bottom) --------------------------------------------------

info.rkt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
(define collection "widgetkit")
44
(define scribblings '(("widgetkit.scrbl" ())))
5-
(define version "0.3.0")
5+
(define version "0.4.0")
66
(define pkg-desc "A curated collection of GUI widgets for Racket")
77
(define pkg-authors '("jrtxio"))
88
(define license 'MIT)

main.rkt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ SOFTWARE.
5050
"private/text-list.rkt"
5151
"private/disclosure.rkt"
5252
"private/image-view.rkt"
53-
"private/progress-dialog.rkt")
53+
"private/progress-dialog.rkt"
54+
"private/log-view.rkt")
5455

5556
(provide (all-from-out "private/status-bar.rkt")
5657
(all-from-out "private/spinner.rkt")
@@ -59,7 +60,8 @@ SOFTWARE.
5960
(all-from-out "private/text-list.rkt")
6061
(all-from-out "private/disclosure.rkt")
6162
(all-from-out "private/image-view.rkt")
62-
(all-from-out "private/progress-dialog.rkt"))
63+
(all-from-out "private/progress-dialog.rkt")
64+
(all-from-out "private/log-view.rkt"))
6365

6466
;; ---------------------------------------------------------------------------
6567
;; Layer 2 — aggregated widgets (maintained upstream, re-exported here)

private/log-view.rkt

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
;; SPDX-License-Identifier: MIT
2+
;; Copyright (c) 2026 jrtxio <jirentianxiang1024@gmail.com>
3+
;; See LICENSE at the repository root for full terms.
4+
5+
#lang racket/base
6+
7+
(require racket/class
8+
racket/gui/base
9+
racket/string)
10+
11+
(provide log-view%)
12+
13+
;; log-view% — a scrolling, read-only, monospace log/console output.
14+
;;
15+
;; Why this exists: building this from core `editor-canvas%` + `text%` is where
16+
;; everyone gets stuck — the canvas won't stretch (parent container trap), it
17+
;; does not auto-scroll to the newest line, making it read-only also blocks
18+
;; programmatic inserts, and long-running logs grow without bound. `log-view%`
19+
;; handles all of it: it stretches to fill its parent by default, appends lines
20+
;; and auto-scrolls to the bottom, stays read-only to the user while still
21+
;; accepting programmatic appends, and trims old lines past `max-lines`.
22+
;;
23+
;; (define log (new log-view% [parent f]))
24+
;; (send log append-line "[boot] ready")
25+
(define log-view%
26+
(class editor-canvas%
27+
(init-field [max-lines 10000]
28+
[monospace? #t]
29+
[wrap? #t]
30+
[read-only? #t])
31+
32+
;; Build the editor first (no `this` needed), then hand it to the canvas.
33+
(define editor (new text%))
34+
(send editor auto-wrap wrap?)
35+
36+
(super-new
37+
[editor editor]
38+
[style (if wrap? '(no-hscroll) '())])
39+
;; editor-canvas% defaults to stretchable in both dimensions, so log-view%
40+
;; fills its parent automatically. Callers can still override min-width,
41+
;; min-height, stretchable-*, etc. via the usual init fields.
42+
43+
(define mono-delta
44+
(and monospace? (make-object style-delta% 'change-family 'modern)))
45+
46+
;; A read-only lock also blocks programmatic edits, so toggle it around
47+
;; each mutation.
48+
(when read-only? (send editor lock #t))
49+
(define (unlock!) (when read-only? (send editor lock #f)))
50+
(define (relock!) (when read-only? (send editor lock #t)))
51+
52+
;; Append a line of text (a trailing newline is ensured) and scroll to it.
53+
;; Non-strings are formatted with ~a.
54+
(define/public (append-line line)
55+
(define s (if (string? line) line (format "~a" line)))
56+
(define text (if (string-suffix? s "\n") s (string-append s "\n")))
57+
(unlock!)
58+
(define start (send editor last-position))
59+
(send editor insert text start)
60+
(when mono-delta
61+
(send editor change-style mono-delta start (send editor last-position)))
62+
(trim!)
63+
(send editor scroll-to-position (send editor last-position))
64+
(relock!))
65+
66+
;; Drop the oldest lines once the buffer exceeds max-lines.
67+
(define (trim!)
68+
(define nl (send editor last-line))
69+
(when (> nl max-lines)
70+
(send editor delete 0 (send editor line-start-position (- nl max-lines)))))
71+
72+
;; Erase the whole log.
73+
(define/public (clear)
74+
(unlock!)
75+
(send editor erase)
76+
(relock!))
77+
78+
;; All log text as a string.
79+
(define/public (get-text)
80+
(send editor get-text))
81+
82+
;; Scroll so the most recent line is visible.
83+
(define/public (scroll-to-bottom)
84+
(send editor scroll-to-position (send editor last-position)))))

test/run.rkt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
(check-true (class? disclosure%) "disclosure% exported")
3535
(check-true (class? image-view%) "image-view% exported")
3636
(check-true (class? progress-dialog%) "progress-dialog% exported")
37+
(check-true (class? log-view%) "log-view% exported")
3738
(check-true (class? table-panel%) "table-panel% re-exported")
3839
(check-true (class? canvas-list%) "canvas-list% re-exported")
3940
(check-true (class? date-text-field%) "date-text-field% re-exported")

0 commit comments

Comments
 (0)