package miaou-core

  1. Overview
  2. Docs
Miaou core/widgets (no drivers, no SDL)

Install

dune-project
 Dependency

Authors

Maintainers

Sources

v0.5.2.tar.gz
md5=60a3b9f181f24572a06a9492532bfdda
sha512=fcc35a275066be2900e6201782faf47503076fa4640f08cf78067835a6f447b74613009e55b2ac799adb7ca46f1bffa261fc5971753f2cc3c6bef327511c7ef6

doc/src/miaou_widgets_layout/grid_layout.ml.html

Source file grid_layout.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
(*****************************************************************************)
(*                                                                           *)
(* SPDX-License-Identifier: MIT                                              *)
(* Copyright (c) 2025 Nomadic Labs <contact@nomadic-labs.com>                *)
(* Copyright (c) 2026 Mathias Bourgoin <mathias.bourgoin@atacama.tech>       *)
(*                                                                           *)
(*****************************************************************************)

module W = Miaou_widgets_display.Widgets
module Helpers = Miaou_helpers.Helpers
module Style_context = Miaou_style.Style_context

type track =
  | Px of int
  | Fr of float
  | Percent of float
  | Auto
  | MinMax of int * int

type placement = {row : int; col : int; row_span : int; col_span : int}

type grid_child = {
  render : size:LTerm_geom.size -> string;
  placement : placement;
}

type t = {
  rows : track list;
  cols : track list;
  row_gap : int;
  col_gap : int;
  padding : Flex_layout.padding;
  children : grid_child list;
}

let default_padding : Flex_layout.padding =
  {left = 0; right = 0; top = 0; bottom = 0}

let create ~rows ~cols ?(row_gap = 0) ?(col_gap = 0)
    ?(padding = default_padding) children =
  {rows; cols; row_gap; col_gap; padding; children}

let cell ~row ~col render =
  {render; placement = {row; col; row_span = 1; col_span = 1}}

let span ~row ~col ~row_span ~col_span render =
  {render; placement = {row; col; row_span; col_span}}

(* Resolve track sizes to concrete pixel values. *)
let resolve_tracks tracks available =
  let n = List.length tracks in
  let sizes = Array.make n 0 in
  let used = ref 0 in
  (* Pass 1: fixed, percent, minmax base *)
  List.iteri
    (fun i t ->
      match t with
      | Px p ->
          let v = max 0 p in
          sizes.(i) <- v ;
          used := !used + v
      | Percent p ->
          let v = max 0 (int_of_float (p /. 100. *. float available)) in
          sizes.(i) <- v ;
          used := !used + v
      | MinMax (mn, _) ->
          let v = max 0 mn in
          sizes.(i) <- v ;
          used := !used + v
      | Fr _ | Auto -> ())
    tracks ;
  (* Pass 2: distribute remaining to Fr/Auto *)
  let remaining = max 0 (available - !used) in
  let total_fr =
    List.fold_left
      (fun acc t ->
        match t with Fr f -> acc +. f | Auto -> acc +. 1. | _ -> acc)
      0.
      tracks
  in
  if total_fr > 0. then
    List.iteri
      (fun i t ->
        match t with
        | Fr f ->
            sizes.(i) <- max 0 (int_of_float (f /. total_fr *. float remaining))
        | Auto ->
            sizes.(i) <-
              max 0 (int_of_float (1. /. total_fr *. float remaining))
        | _ -> ())
      tracks ;
  (* Pass 3: give leftover to MinMax up to their max *)
  let total_used = Array.fold_left ( + ) 0 sizes in
  let leftover = ref (max 0 (available - total_used)) in
  if !leftover > 0 then
    List.iteri
      (fun i t ->
        match t with
        | MinMax (_, mx) ->
            let room = max 0 (mx - sizes.(i)) in
            let give = min room !leftover in
            sizes.(i) <- sizes.(i) + give ;
            leftover := !leftover - give
        | _ -> ())
      tracks ;
  sizes

let split_lines s = String.split_on_char '\n' s

let truncate_visible s width =
  let idx = Helpers.visible_byte_index_of_pos s width in
  String.sub s 0 idx

let pad_line line width =
  let vis = W.visible_chars_count line in
  if vis >= width then truncate_visible line width
  else line ^ String.make (width - vis) ' '

let pad_block lines ~width ~height =
  let padded = List.map (fun l -> pad_line l width) lines in
  let len = List.length padded in
  if len >= height then List.filteri (fun i _ -> i < height) padded
  else padded @ List.init (height - len) (fun _ -> String.make width ' ')

(* Compute the pixel width of a column span including inner gaps. *)
let span_width col_sizes col_gap start count =
  let w = ref 0 in
  for c = start to start + count - 1 do
    w := !w + col_sizes.(c)
  done ;
  !w + max 0 ((count - 1) * col_gap)

(* Compute the pixel height of a row span including inner gaps. *)
let span_height row_sizes row_gap start count =
  let h = ref 0 in
  for r = start to start + count - 1 do
    h := !h + row_sizes.(r)
  done ;
  !h + max 0 ((count - 1) * row_gap)

let render t ~size =
  let inner_w = size.LTerm_geom.cols - t.padding.left - t.padding.right in
  let inner_h = size.LTerm_geom.rows - t.padding.top - t.padding.bottom in
  let n_rows = List.length t.rows in
  let n_cols = List.length t.cols in
  if n_rows = 0 || n_cols = 0 then ""
  else
    let col_gap_total = max 0 ((n_cols - 1) * t.col_gap) in
    let row_gap_total = max 0 ((n_rows - 1) * t.row_gap) in
    let col_sizes = resolve_tracks t.cols (max 0 (inner_w - col_gap_total)) in
    let row_sizes = resolve_tracks t.rows (max 0 (inner_h - row_gap_total)) in
    (* Pre-render all children into blocks of lines. *)
    let child_count = List.length t.children in
    let rendered =
      t.children
      |> List.mapi (fun idx child -> (idx, child))
      |> List.filter_map (fun (idx, child) ->
          let p = child.placement in
          if p.row < 0 || p.row >= n_rows || p.col < 0 || p.col >= n_cols then
            None
          else
            let rs = min p.row_span (n_rows - p.row) in
            let cs = min p.col_span (n_cols - p.col) in
            let w = span_width col_sizes t.col_gap p.col cs in
            let h = span_height row_sizes t.row_gap p.row rs in
            let child_size = {LTerm_geom.rows = max 0 h; cols = max 0 w} in
            (* Set up style context for this child with index info for :nth-child selectors *)
            let lines =
              Style_context.with_child_context
                ~widget_name:"grid-cell"
                ~index:idx
                ~count:child_count
                (fun () -> split_lines (child.render ~size:child_size))
            in
            let block = pad_block lines ~width:w ~height:h in
            Some (p, cs, rs, block))
    in
    (* Build a "covered" map: for each cell, which rendered child covers it
     and what is the column offset within the span. *)
    let owner = Array.init n_rows (fun _ -> Array.make n_cols None) in
    List.iter
      (fun (p, cs, rs, block) ->
        for r = p.row to p.row + rs - 1 do
          for c = p.col to p.col + cs - 1 do
            owner.(r).(c) <- Some (p, cs, block)
          done
        done)
      rendered ;
    (* Assemble output line by line. *)
    let buf = Buffer.create (inner_w * inner_h) in
    let left_pad = String.make t.padding.left ' ' in
    let blank_line = String.make inner_w ' ' in
    (* Top padding *)
    for _ = 1 to t.padding.top do
      Buffer.add_string buf left_pad ;
      Buffer.add_string buf blank_line ;
      Buffer.add_char buf '\n'
    done ;
    (* For each grid row *)
    for row = 0 to n_rows - 1 do
      let row_h = row_sizes.(row) in
      (* line offset within this grid row's portion of spanning blocks *)
      let row_line_offset =
        let off = ref 0 in
        (* lines consumed by prior rows in a row-spanning block are accounted
         for by computing the offset from the placement start row. *)
        off := 0 ;
        !off
      in
      ignore row_line_offset ;
      for line_idx = 0 to row_h - 1 do
        Buffer.add_string buf left_pad ;
        let col = ref 0 in
        while !col < n_cols do
          if !col > 0 then Buffer.add_string buf (String.make t.col_gap ' ') ;
          match owner.(row).(!col) with
          | None ->
              Buffer.add_string buf (String.make col_sizes.(!col) ' ') ;
              incr col
          | Some (p, cs, block) ->
              if !col = p.col then begin
                (* This is the start column of the span — output the full
                 span width from the pre-rendered block. *)
                let abs_line =
                  let off = ref 0 in
                  for r = p.row to row - 1 do
                    off := !off + row_sizes.(r) + t.row_gap
                  done ;
                  !off + line_idx
                in
                let line =
                  match List.nth_opt block abs_line with
                  | Some l -> l
                  | None ->
                      String.make (span_width col_sizes t.col_gap p.col cs) ' '
                in
                Buffer.add_string buf line ;
                col := !col + cs
              end
              else begin
                (* We're inside a span but not at its start column. The start
                 column already emitted the full span, so skip. However this
                 should not happen because we jump col by cs above. If it
                 does, emit blank. *)
                Buffer.add_string buf (String.make col_sizes.(!col) ' ') ;
                incr col
              end
        done ;
        if row < n_rows - 1 || line_idx < row_h - 1 then
          Buffer.add_char buf '\n'
      done ;
      (* Row gap — each gap line is a full blank row.  The '\n' comes
         AFTER the blank so the next row's first content line starts on
         its own line. *)
      if row < n_rows - 1 then
        for _ = 1 to t.row_gap do
          Buffer.add_string buf left_pad ;
          Buffer.add_string buf blank_line ;
          Buffer.add_char buf '\n'
        done
    done ;
    Buffer.contents buf

[@@@enforce_exempt] (* non-widget module *)