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/flex_layout.ml.html

Source file flex_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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
(*****************************************************************************)
(*                                                                           *)
(* SPDX-License-Identifier: MIT                                              *)
(* Copyright (c) 2025 Nomadic Labs <contact@nomadic-labs.com>                *)
(* Copyright (c) 2026 Mathias Bourgoin <mathias.bourgoin@atacama.tech>       *)
(*                                                                           *)
(*****************************************************************************)

[@@@warning "-32-34-37-69"]

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

type direction = Row | Column

type align_items = Start | Center | End | Stretch

type justify = Start | Center | End | Space_between | Space_around

type spacing = {h : int; v : int}

type padding = {left : int; right : int; top : int; bottom : int}

type basis = Auto | Px of int | Ratio of float | Percent of float | Fill

type size_hint = {width : int option; height : int option}

type child = {
  render : size:LTerm_geom.size -> string;
  basis : basis;
  cross : size_hint option;
}

type child_constraint = {
  index : int;
  min_size : int option;
  max_size : int option;
}

type t = {
  direction : direction;
  align_items : align_items;
  justify : justify;
  gap : spacing;
  padding : padding;
  constraints : child_constraint list;
  children : child list;
}

let default_padding = {left = 0; right = 0; top = 0; bottom = 0}

let default_gap = {h = 0; v = 0}

let create ?(direction = Row) ?(align_items : align_items = Start)
    ?(justify : justify = Start) ?(gap = default_gap)
    ?(padding = default_padding) ?(constraints = []) children =
  {direction; align_items; justify; gap; padding; constraints; children}

let clamp v ~min_v ~max_v = max min_v (min max_v v)

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

let pad_lines lines ~width =
  List.map
    (fun line ->
      let vis = W.visible_chars_count line in
      if vis >= width then truncate_visible line width
      else line ^ String.make (width - vis) ' ')
    lines

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

let rec take n lst =
  if n <= 0 then []
  else match lst with [] -> [] | x :: xs -> x :: take (n - 1) xs

let pad_block ?(align : align_items = Start) lines ~width ~height =
  let lines =
    lines
    |> List.map (fun line ->
        let vis = W.visible_chars_count line in
        if vis > width then truncate_visible line width else line)
    |> ( function ls when align = Stretch -> pad_lines ls ~width | ls -> ls )
    |> take height
  in
  let missing = max 0 (height - List.length lines) in
  let blanks n = List.init n (fun _ -> String.make width ' ') in
  match align with
  | Start -> lines @ blanks missing
  | End -> blanks missing @ lines
  | Center ->
      let top = missing / 2 in
      blanks top @ lines @ blanks (missing - top)
  | Stretch ->
      (* For now, stretch behaves like start; future iterations could distribute extra lines. *)
      lines @ blanks missing

let apply_constraints constraints sizes =
  let arr = Array.of_list sizes in
  List.iter
    (fun c ->
      if c.index >= 0 && c.index < Array.length arr then begin
        (match c.min_size with
        | Some mn -> arr.(c.index) <- max mn arr.(c.index)
        | None -> ()) ;
        match c.max_size with
        | Some mx -> arr.(c.index) <- min mx arr.(c.index)
        | None -> ()
      end)
    constraints ;
  Array.to_list arr

let compute_sizes direction padding gap size children constraints =
  let available_main =
    match direction with
    | Row ->
        size.LTerm_geom.cols - padding.left - padding.right
        - max 0 ((List.length children - 1) * gap.h)
    | Column ->
        size.LTerm_geom.rows - padding.top - padding.bottom
        - max 0 ((List.length children - 1) * gap.v)
  in
  let fixed, ratios, percents, fills =
    List.fold_left
      (fun (fix, rat, per, fil) c ->
        match c.basis with
        | Px n -> (fix + n, rat, per, fil)
        | Ratio r -> (fix, rat +. r, per, fil)
        | Percent p -> (fix, rat, per +. p, fil)
        | Fill -> (fix, rat, per, fil + 1)
        | Auto -> (fix, rat, per, fil + 1))
      (0, 0., 0., 0)
      children
  in
  let remaining =
    max
      0
      (available_main - fixed
      - int_of_float (percents *. float available_main /. 100.))
  in
  let alloc_child c =
    match c.basis with
    | Px n -> n
    | Ratio r when ratios > 0. -> int_of_float (r /. ratios *. float remaining)
    | Ratio _ -> 0
    | Percent p -> int_of_float (p /. 100. *. float available_main)
    | Fill | Auto -> if fills > 0 then remaining / max 1 fills else remaining
  in
  let sizes = List.map alloc_child children in
  apply_constraints constraints sizes

let distribute direction padding justify gap child_sizes size =
  let children = List.length child_sizes in
  let inner =
    match direction with
    | Row -> size.LTerm_geom.cols - padding.left - padding.right
    | Column -> size.LTerm_geom.rows - padding.top - padding.bottom
  in
  let used =
    List.fold_left ( + ) 0 child_sizes
    + max 0 ((children - 1) * if direction = Row then gap.h else gap.v)
  in
  let extra = max 0 (inner - used) in
  match justify with
  | Start -> (0, gap, extra)
  | End -> (extra, gap, 0)
  | Center ->
      let lead = extra / 2 in
      (lead, gap, extra - lead)
  | Space_between ->
      if children <= 1 then (0, gap, extra)
      else
        let between =
          if direction = Row then
            {gap with h = gap.h + (extra / (children - 1))}
          else {gap with v = gap.v + (extra / (children - 1))}
        in
        (0, between, extra mod max 1 (children - 1))
  | Space_around ->
      let lead = extra / (children + 1) in
      let between =
        if direction = Row then {gap with h = gap.h + lead}
        else {gap with v = gap.v + lead}
      in
      (lead, between, extra - (lead * (children + 1)))

let render_row t ~size =
  let child_sizes =
    compute_sizes Row t.padding t.gap size t.children t.constraints
  in
  let leading, gap, trailing_extra =
    distribute Row t.padding t.justify t.gap child_sizes size
  in
  let max_h = size.LTerm_geom.rows - t.padding.top - t.padding.bottom in
  let child_count = List.length t.children in
  let rendered =
    List.mapi
      (fun i (c, w) ->
        let child_size = {LTerm_geom.rows = max_h; cols = max 0 w} in
        (* Set up style context for this child with index info for :nth-child selectors *)
        let raw =
          Style_context.with_child_context
            ~widget_name:"flex-child"
            ~index:i
            ~count:child_count
            (fun () -> split_lines (c.render ~size:child_size))
        in
        let block = pad_block ~align:t.align_items raw ~width:w ~height:max_h in
        block)
      (List.combine t.children child_sizes)
  in
  let inner_width = size.LTerm_geom.cols - t.padding.left - t.padding.right in
  let lines =
    List.init max_h (fun row ->
        let buf = Buffer.create (size.LTerm_geom.cols + 2) in
        Buffer.add_string buf (String.make (t.padding.left + leading) ' ') ;
        let rec emit idx blocks widths =
          match (blocks, widths) with
          | [], _ | _, [] -> ()
          | blk :: rest_blk, w :: rest_w ->
              let line =
                match List.nth_opt blk row with Some s -> s | None -> ""
              in
              if idx > 0 then Buffer.add_string buf (String.make gap.h ' ') ;
              Buffer.add_string buf line ;
              (* Pad line to allocated width for proper column alignment *)
              let vis = W.visible_chars_count line in
              if vis < w then Buffer.add_string buf (String.make (w - vis) ' ') ;
              emit (idx + 1) rest_blk rest_w
        in
        emit 0 rendered child_sizes ;
        let used =
          List.fold_left ( + ) 0 child_sizes
          + max 0 ((List.length child_sizes - 1) * gap.h)
        in
        let consumed = leading + used in
        let remaining = max 0 (inner_width - consumed - trailing_extra) in
        Buffer.add_string
          buf
          (String.make (remaining + t.padding.right + trailing_extra) ' ') ;
        Buffer.contents buf)
  in
  lines

let render_column t ~size =
  let child_sizes =
    compute_sizes Column t.padding t.gap size t.children t.constraints
  in
  let max_w = size.LTerm_geom.cols - t.padding.left - t.padding.right in
  let leading, gap, trailing_extra =
    distribute Column t.padding t.justify t.gap child_sizes size
  in
  let child_count = List.length t.children in
  let blocks =
    List.mapi
      (fun i (c, h) ->
        let child_size = {LTerm_geom.rows = max 0 h; cols = max_w} in
        (* Set up style context for this child with index info for :nth-child selectors *)
        let raw =
          Style_context.with_child_context
            ~widget_name:"flex-child"
            ~index:i
            ~count:child_count
            (fun () -> split_lines (c.render ~size:child_size))
        in
        let blk = pad_block ~align:t.align_items raw ~width:max_w ~height:h in
        blk)
      (List.combine t.children child_sizes)
  in
  let gap_lines =
    if gap.v <= 0 then [] else List.init gap.v (fun _ -> String.make max_w ' ')
  in
  let rec interleave = function
    | [] -> []
    | [b] -> b
    | b :: rest -> b @ gap_lines @ interleave rest
  in
  let rows = size.LTerm_geom.rows - t.padding.top - t.padding.bottom in
  let spaced =
    interleave blocks |> fun lines ->
    let top = List.init leading (fun _ -> String.make max_w ' ') in
    let bottom = List.init trailing_extra (fun _ -> String.make max_w ' ') in
    top @ lines @ bottom
  in
  let all_lines = take rows spaced in
  let with_pad =
    List.map
      (fun line ->
        let vis = W.visible_chars_count line in
        let trimmed =
          if vis > max_w then truncate_visible line max_w else line
        in
        let trimmed_vis = if vis > max_w then max_w else vis in
        let free = max 0 (max_w - trimmed_vis) in
        let left_pad, right_pad =
          match t.align_items with
          | Start | Stretch -> (0, free)
          | End -> (free, 0)
          | Center ->
              let l = free / 2 in
              (l, free - l)
        in
        String.make (t.padding.left + left_pad) ' '
        ^ trimmed
        ^ String.make (t.padding.right + right_pad) ' ')
      all_lines
  in
  with_pad

let render t ~size =
  let lines =
    match t.direction with
    | Row -> render_row t ~size
    | Column -> render_column t ~size
  in
  Helpers.concat_lines lines

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