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

Source file list_widget.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
(*****************************************************************************)
(*                                                                           *)
(* SPDX-License-Identifier: MIT                                              *)
(* Copyright (c) 2025 Nomadic Labs <contact@nomadic-labs.com>                *)
(* Copyright (c) 2026 Mathias Bourgoin <mathias.bourgoin@atacama.tech>       *)
(*                                                                           *)
(*****************************************************************************)

module W = Widgets

type item = {
  id : string option;
  label : string;
  children : item list;
  selectable : bool;
}

module String_set = Set.Make (String)

type t = {
  items : item list;
  cursor : int list;
  expanded : String_set.t;
  indent : int;
}

let item ?id ?(selectable = true) ?(children = []) label =
  {id; label; children; selectable}

let group ?id ?(selectable = false) label children =
  {id; label; children; selectable}

let path_key path = String.concat "." (List.map string_of_int path)

let rec collect_expand_all path acc items =
  List.fold_left
    (fun acc (idx, it) ->
      let p = path @ [idx] in
      let acc =
        if it.children = [] then acc else String_set.add (path_key p) acc
      in
      if it.children = [] then acc else collect_expand_all p acc it.children)
    acc
    (List.mapi (fun i it -> (i, it)) items)

let create ?(indent = 2) ?(expand_all = true) items =
  let expanded =
    if expand_all then collect_expand_all [] String_set.empty items
    else String_set.empty
  in
  {items; cursor = [0]; expanded; indent}

let set_items t items =
  let valid = collect_expand_all [] String_set.empty items in
  let expanded = String_set.inter t.expanded valid in
  {t with items; expanded}

type flat_item = {
  path : int list;
  depth : int;
  item : item;
  has_children : bool;
  is_expanded : bool;
}

let rec flatten_items ?(depth = 0) path expanded items =
  let rec loop acc = function
    | [] -> List.rev acc
    | (idx, it) :: rest ->
        let p = path @ [idx] in
        let has_children = it.children <> [] in
        let is_expanded =
          has_children && String_set.mem (path_key p) expanded
        in
        let acc =
          {path = p; depth; item = it; has_children; is_expanded} :: acc
        in
        let acc =
          if is_expanded then
            let children =
              flatten_items ~depth:(depth + 1) p expanded it.children
            in
            List.rev_append children acc
          else acc
        in
        loop acc rest
  in
  loop [] (List.mapi (fun i it -> (i, it)) items)

let current_flat t = flatten_items [] t.expanded t.items

let current_index t =
  let flat = current_flat t in
  let rec find idx = function
    | [] -> 0
    | x :: xs -> if x.path = t.cursor then idx else find (idx + 1) xs
  in
  find 0 flat

let visible_count t = List.length (current_flat t)

let cursor_index t = current_index t

let set_cursor_by_index t idx =
  let flat = current_flat t in
  match List.nth_opt flat idx with
  | Some f -> {t with cursor = f.path}
  | None -> t

let set_cursor_index t idx = set_cursor_by_index t idx

let selected t =
  let flat = current_flat t in
  List.find_opt (fun f -> f.path = t.cursor) flat
  |> Option.map (fun f -> f.item)

let selected_path t =
  let flat = current_flat t in
  if List.exists (fun f -> f.path = t.cursor) flat then Some t.cursor else None

let toggle t =
  let flat = current_flat t in
  match List.find_opt (fun f -> f.path = t.cursor) flat with
  | None -> t
  | Some f ->
      if not f.has_children then t
      else
        let key = path_key f.path in
        let expanded =
          if String_set.mem key t.expanded then String_set.remove key t.expanded
          else String_set.add key t.expanded
        in
        {t with expanded}

let expand_all t =
  let expanded = collect_expand_all [] String_set.empty t.items in
  {t with expanded}

let collapse_all t = {t with expanded = String_set.empty}

let render_line ~focus current f indent =
  let marker =
    if f.has_children then if f.is_expanded then "▾" else "▸" else " "
  in
  let marker =
    if Lazy.force W.use_ascii_borders then
      if f.has_children then if f.is_expanded then "v" else ">" else " "
    else marker
  in
  let pad = String.make (f.depth * indent) ' ' in
  let line = pad ^ marker ^ " " ^ f.item.label in
  if focus && current then W.themed_selection line else line

let render t ~focus =
  let flat = current_flat t in
  let current = t.cursor in
  flat
  |> List.map (fun f -> render_line ~focus (f.path = current) f t.indent)
  |> Miaou_helpers.Helpers.concat_lines

let move_cursor t delta =
  let flat = current_flat t in
  let total = List.length flat in
  if total = 0 then t
  else
    let idx = current_index t in
    let idx = max 0 (min (total - 1) (idx + delta)) in
    set_cursor_by_index t idx

let handle_key t ~key =
  match key with
  | "Down" | "j" -> move_cursor t 1
  | "Up" | "k" -> move_cursor t (-1)
  | "Right" | "l" ->
      let flat = current_flat t in
      let is_collapsed =
        List.find_opt (fun f -> f.path = t.cursor) flat
        |> Option.map (fun f -> f.has_children && not f.is_expanded)
        |> Option.value ~default:false
      in
      if is_collapsed then toggle t else move_cursor t 1
  | "Left" | "h" -> (
      let flat = current_flat t in
      let is_expanded =
        List.find_opt (fun f -> f.path = t.cursor) flat
        |> Option.map (fun f -> f.has_children && f.is_expanded)
        |> Option.value ~default:false
      in
      if is_expanded then toggle t
      else
        (* Move to parent if any *)
        let parent_path =
          match List.rev t.cursor with
          | _ :: parent_rev -> Some (List.rev parent_rev)
          | [] -> None
        in
        match parent_path with Some p -> {t with cursor = p} | None -> t)
  | "Enter" | " " -> toggle t
  | _ -> t

let () = Miaou_registry.register ~name:"list" ~mli:[%blob "list_widget.mli"] ()