wig.scm (12261B)
1 ;;; Wig --- Widgets in Guile 2 ;;; Copyright © 2026 Luke Willis <lukejw@monastech.xyz> 3 ;;; 4 ;;; This file is part of Wig. 5 ;;; 6 ;;; Wig is free software: you can redistribute it and/or modify it under 7 ;;; the terms of the GNU General Public License as published by the Free 8 ;;; Software Foundation, either version 3 of the License, or (at your option) 9 ;;; any later version. 10 ;;; 11 ;;; Wig is distributed in the hope that it will be useful, but WITHOUT 12 ;;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 ;;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 ;;; for more details. 15 ;;; 16 ;;; You should have received a copy of the GNU General Public License along 17 ;;; with Wig. If not, see <https://www.gnu.org/licenses/>. 18 19 (define-module (wig) 20 #:use-module (srfi srfi-1) 21 #:use-module (srfi srfi-9) 22 #:use-module (ice-9 match) 23 #:use-module (system foreign) 24 #:use-module (rnrs bytevectors) 25 #:export (widget 26 widget? 27 widget-hexpand 28 widget-vexpand 29 widget-width 30 widget-height 31 widget-contents 32 33 anchors 34 anchors? 35 anchors-top 36 anchors-bottom 37 anchors-left 38 anchors-right 39 40 window 41 window? 42 window-label 43 window-anchors 44 window-width 45 window-height 46 47 configuration 48 configuration? 49 configuration-windows)) 50 51 ;; Widget-related records 52 53 (define-record-type <widget> 54 (make-widget ghost valign hexpand vexpand width height margin color contents) 55 widget? 56 (ghost widget-ghost) 57 (valign widget-valign) 58 (hexpand widget-hexpand) 59 (vexpand widget-vexpand) 60 (width widget-width) 61 (height widget-height) 62 (margin widget-margin) 63 (color widget-color) 64 (contents widget-contents)) 65 66 (define (widgets-predict-layout widgets) 67 "Find the minimum width to contain a list of widgets." 68 (let loop ((queue widgets) 69 (prev #f) 70 (reserved 0) 71 (slots 0)) 72 (if (null? queue) 73 (cons slots (+ reserved prev)) 74 (let* ((current (car queue)) 75 (width (widget-width current)) 76 (height (widget-height current)) 77 (margin (widget-margin current))) 78 (if (widget-hexpand current) 79 (loop (cdr queue) 80 margin 81 (+ reserved (max (or prev 0) margin)) 82 (+ slots 1)) 83 (loop (cdr queue) 84 margin 85 (+ reserved (max (or prev 0) margin) width) 86 slots)))))) 87 88 (define* (widget #:key 89 (ghost #f) 90 (valign #t) 91 (hexpand #f) 92 (vexpand #f) 93 (width 0) 94 (height 0) 95 (margin 0) 96 (color #xFF00FF) 97 (contents #f)) 98 (make-widget ghost 99 valign 100 hexpand 101 vexpand 102 ;; Expand to fit children if too small 103 (if (list? contents) 104 (match (widgets-predict-layout contents) 105 ((slots . reserved) 106 (if (< width reserved) 107 reserved 108 width))) 109 width) 110 (if (list? contents) 111 (fold (lambda (in out) 112 (max out (+ (widget-height in) 113 (* (widget-margin in) 2)))) 114 height 115 contents) 116 height) 117 margin 118 color 119 contents)) 120 121 ;; Window-related records 122 123 (define-record-type <anchors> 124 (make-anchors top bottom left right) 125 anchors? 126 (top anchors-top) 127 (bottom anchors-bottom) 128 (left anchors-left) 129 (right anchors-right)) 130 131 (define* (anchors #:key 132 (top #f) 133 (bottom #f) 134 (left #f) 135 (right #f)) 136 (unless (and (boolean? top) 137 (boolean? bottom) 138 (boolean? left) 139 (boolean? right)) 140 (error "Invalid anchors!")) 141 (make-anchors top bottom left right)) 142 143 (define-record-type <window> 144 (make-window label anchors exclusive width height margin widgets) 145 window? 146 (label window-label) 147 (anchors window-anchors) 148 (exclusive window-exclusive) 149 (width window-width) 150 (height window-height) 151 (margin window-margin) 152 (widgets window-widgets)) 153 154 (define* (window #:key 155 (label "wig_window") 156 (anchors (anchors)) 157 (exclusive #f) 158 (width 0) 159 (height 0) 160 (margin 0) 161 (widgets (list (widget #:hexpand #t 162 #:vexpand #t 163 #:width 0 164 #:height 0 165 #:color #xFFFFFF)))) 166 (unless (and (string? label) 167 (anchors? anchors) 168 (boolean? exclusive) 169 (integer? width) 170 (integer? height) 171 (integer? margin) 172 (list? widgets) 173 (every widget? widgets)) 174 (error "Invalid window!")) 175 (when (and (anchors-top anchors) 176 (anchors-bottom anchors) 177 (anchors-left anchors) 178 (anchors-right anchors) 179 exclusive) 180 (error "A window cannot anchor to all sides and be exclusive.")) 181 (make-window label anchors exclusive width height margin widgets)) 182 183 (define-record-type <configuration> 184 (make-configuration font windows) 185 configuration? 186 (font configuration-font) 187 (windows configuration-windows)) 188 189 (define* (configuration #:key 190 (font "monospace:size=14") 191 (windows '())) 192 (unless (and (string? font) 193 (list? windows) 194 (every window? windows)) 195 (error "Invalid configuration!")) 196 (make-configuration font windows)) 197 198 ;; Private compilation types/functions 199 200 (define-record-type <region> 201 (make-region x y width height) 202 region? 203 (x region-x) 204 (y region-y) 205 (width region-width) 206 (height region-height)) 207 208 (define* (region #:key 209 x 210 y 211 width 212 height) 213 (make-region x y width height)) 214 215 (define (count-widgets widgets) 216 "Return the total number of widgets in a tree of widgets." 217 (cond ((pair? widgets) 218 (+ (count-widgets (car widgets)) 219 (count-widgets (cdr widgets)))) 220 ((widget? widgets) 221 (+ 1 (count-widgets (widget-contents widgets)))) 222 (else 0))) 223 224 225 (define (flatten-widgets widgets container) 226 (let* ((data (widgets-predict-layout widgets)) 227 (slots (car data)) 228 (reserved (cdr data)) 229 (slot-width (if (> slots 0) 230 (/ (- (region-width container) reserved) 231 slots) 232 0))) 233 (let loop ((queue widgets) 234 (x (region-x container)) 235 (y (region-y container)) 236 (prev #f) 237 (result '())) 238 (if (null? queue) 239 result 240 (let* ((current (car queue)) 241 (margin (widget-margin current)) 242 (width (if (widget-hexpand current) 243 slot-width 244 (widget-width current))) 245 (height (if (widget-vexpand current) 246 (- (region-height container) 247 (* margin 2)) 248 (widget-height current))) 249 (nx (+ x (max (or prev 0) margin))) 250 (ny (+ y 251 (if (and (not (widget-vexpand current)) 252 (widget-valign current)) 253 (- (/ (region-height container) 2) 254 (/ height 2)) 255 margin) )) 256 ) 257 (loop (cdr queue) 258 (+ nx width) 259 y 260 margin 261 (cons (cons current 262 (region #:x nx 263 #:y ny 264 #:width width 265 #:height height)) 266 result))))))) 267 268 (define (flatten-widgets-old widgets reg) 269 "Fit a list of widgets inside a region, returning widget/region pairs." 270 (let* ((data (fold (lambda (input output) 271 (if (widget-hexpand input) 272 (cons (+ (car output) 1) 273 (cdr output)) 274 (cons (car output) 275 (+ (cdr output) 276 (widget-width input) 277 (* (widget-margin input) 2))))) 278 '(0 . 0) 279 widgets)) 280 (num-hexpand (car data)) 281 (reserved-width (cdr data)) 282 (hexpand-size (if (> num-hexpand 0) 283 (/ (- (region-width reg) reserved-width) 284 num-hexpand) 285 0))) 286 (let loop ((queue widgets) 287 (result '()) 288 (x (region-x reg)) 289 (y (region-y reg))) 290 (if (null? queue) 291 result 292 (let* ((widget (car queue)) 293 (width (if (widget-hexpand widget) 294 hexpand-size 295 (widget-width widget))) 296 (height (if (widget-vexpand widget) 297 (region-height reg) 298 (widget-height widget)))) 299 (loop (cdr queue) 300 (cons (cons widget 301 (region #:x (+ x (widget-margin widget)) 302 #:y (+ y (if (and (not (widget-vexpand widget)) 303 (widget-valign widget)) 304 (- (/ (region-height reg) 2) 305 (/ height 2)) 306 0)) 307 #:width width 308 #:height height)) 309 result) 310 (+ x width (* (widget-margin widget) 2)) 311 y)))))) 312 313 ;; TODO: Look into using bstructs 314 315 (define (bools-to-bitflags bools) 316 (fold (lambda (in out) 317 (+ (if in 1 0) 318 (ash out 1))) 319 0 320 bools)) 321 322 (define (compile-widgets widgets width height bv) 323 "Fit a tree of widgets into a region, writing results into a bytevector." 324 (let loop ((queue (flatten-widgets widgets 325 (region #:x 0 326 #:y 0 327 #:width width 328 #:height height))) 329 (result '()) 330 (index 0)) 331 (if (null? queue) 332 result 333 (let* ((current (car queue)) 334 (widget (car current)) 335 (region (cdr current))) 336 ;; struct wig_widget { 337 ;; uint32_t x; 338 ;. uint32_t y; 339 ;; uint32_t width; 340 ;; uint32_t height; 341 ;; uint32_t color; 342 ;; uint32_t flags; 343 ;; } 344 (bytevector-u32-native-set! bv (+ index (* 4 0)) (region-x region)) 345 (bytevector-u32-native-set! bv (+ index (* 4 1)) (region-y region)) 346 (bytevector-u32-native-set! bv (+ index (* 4 2)) (region-width region)) 347 (bytevector-u32-native-set! bv (+ index (* 4 3)) (region-height region)) 348 (bytevector-u32-native-set! bv (+ index (* 4 4)) (widget-color widget)) 349 (bytevector-u32-native-set! bv (+ index (* 4 5)) 350 (bools-to-bitflags 351 (list 352 (widget-ghost widget)))) 353 ;; Iterate 354 (loop (if (list? (widget-contents widget)) 355 (append (flatten-widgets (widget-contents widget) 356 region) 357 (cdr queue)) 358 (cdr queue)) 359 (cons region result) 360 (+ index (* 4 6)))))))