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)))))))