package tiny_appkits
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
Application engines from scratch: a spreadsheet, rich text, paint, draw, CAD, editors and more
Install
dune-project
Dependency
Authors
Maintainers
Sources
0.3.6.tar.gz
md5=7c636383d146d30ac6f2fa234a6253c8
sha512=c79f3823c5f8f57e5038eb640d487c61168b84aa07c61999d6622ef9fd0c890e2b03b4c6a7cdbbe9352a49e25dda00ac7bb14693cee8e3d7beeed251351a2af0
doc/src/tiny_appkits.appkit_richtext/Page.ml.html
Source file Page.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 323 324 325 326 327 328 329 330 331 332 333 334(* Claude Code * * Copyright (C) 2026 Yoann Padioleau * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * (LGPL) as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. *) (* See Page.mli *) (*****************************************************************************) (* Types *) (*****************************************************************************) type metrics = Style.t -> string -> float type align = Left | Center | Right | Justify type glyph = { offset : int; text : string; style : Style.t; x : float; baseline : float; advance : float; } type line = { top : float; height : float; baseline : float; cells : glyph list; (* the offsets it covers: [first, stop) *) first : int; stop : int; } type t = { lines : line list; length : int } (* a line is as tall as its tallest look, with some air, and its * baseline sits one em of that look below its top *) let line_height size = size *. 1.4 (*****************************************************************************) (* From characters to words to lines *) (*****************************************************************************) (* a character, where it is in the text, its look and its width *) type cell = { at : int; ch : string; look : Style.t; w : float } (* a word is its letters and the spaces after them; a newline ends * the word it follows, and the line with it *) type word = { parts : cell list; ink : float; total : float; hard : bool } let words_of cells = let finish acc cur hard = if cur = [] then acc else let parts = List.rev cur in let is_space c = c.ch = " " || c.ch = "\n" in let ink = (* the width up to the last letter: the spaces after it may hang past the edge of the line *) let rec go w last = function | [] -> last | c :: rest -> let w = w +. c.w in go w (if is_space c then last else w) rest in go 0. 0. parts in { parts; ink; total = List.fold_left (fun a c -> a +. c.w) 0. parts; hard } :: acc in let rec go acc cur seen_space = function | [] -> List.rev (finish acc cur false) | c :: rest when c.ch = "\n" -> go (finish acc (c :: cur) true) [] false rest | c :: rest when c.ch = " " -> go acc (c :: cur) true rest | c :: rest -> (* a letter after spaces starts the next word *) if seen_space then go (finish acc cur false) [ c ] false rest else go acc (c :: cur) false rest in go [] [] false cells (* Greedy, word by word: what Bravo did and what Word does. A word * that does not fit goes to the next line; a word longer than the * whole line is broken where it reaches the edge. *) let lines_of ~width words = let lines = ref [] and cur = ref [] and x = ref 0. and last_hard = ref false in let close () = lines := List.rev !cur :: !lines; cur := []; x := 0. in List.iter (fun wd -> if !cur <> [] && !x +. wd.ink > width then close (); if wd.ink > width then (* too long for any line: as much as fits, then the rest *) List.iter (fun c -> if !cur <> [] && !x +. c.w > width && c.ch <> " " then close (); cur := c :: !cur; x := !x +. c.w) wd.parts else begin cur := List.rev_append wd.parts !cur; x := !x +. wd.total end; if wd.hard then close (); last_hard := wd.hard) words; (* the last line -- even an empty one, after a final newline or in an empty text, since the caret has to have somewhere to be *) if !cur <> [] || !lines = [] || !last_hard then close (); List.rev !lines (*****************************************************************************) (* Layout *) (*****************************************************************************) (* A line's slack, given to where the alignment says. [last] is whether * it ends its paragraph (a newline, or the end of the text), which is * set Left even when justifying. *) let aligned align ~width ~last (glyphs : glyph list) = let ink = (* up to the end of the last glyph that is not a space: the spaces a line was broken after hang past the edge, and are not counted *) List.fold_left (fun acc (g : glyph) -> if g.text = " " || g.text = "\n" then acc else g.x +. g.advance) 0. glyphs in let slack = max 0. (width -. ink) in let shift dx = List.map (fun (g : glyph) -> { g with x = g.x +. dx }) glyphs in match align with | Left -> glyphs | Center -> shift (slack /. 2.) | Right -> shift slack | Justify when last -> glyphs | Justify -> (* the spaces between words -- not the ones hanging at the end -- each get an equal share *) let inner = List.filter (fun (g : glyph) -> g.text = " " && g.x +. g.advance <= ink) glyphs in let n = List.length inner in if n = 0 then glyphs else let extra = slack /. float_of_int n in let _, out = List.fold_left (fun (added, acc) (g : glyph) -> let g' = { g with x = g.x +. added } in let is_inner = g.text = " " && g.x +. g.advance <= ink in if is_inner then (added +. extra, { g' with advance = g.advance +. extra } :: acc) else (added, g' :: acc)) (0., []) glyphs in List.rev out (* The lines laid round boxes, one at a time (see the .mli): each as * its segments -- (cells, where the stretch starts, how wide it is) -- * and its top. A line fills the widest stretch the boxes leave it, or, * [both], every stretch wide enough, left to right: a text on both * sides of a box. A line's height is guessed from its first letter's * look before it is filled, which is the look that decides it in all * but mixed lines. *) let lines_around ~both ~width ~around ~size_of words = let min_w = 40. in (* the stretches of [0, width] the boxes reaching [y, y+h) leave, left to right, and those boxes *) let free y h = let blocking = List.filter (fun (_, y0, _, y1) -> y0 < y +. h && y1 > y) around in let cuts = List.sort compare (List.map (fun (x0, _, x1, _) -> (Float.max 0. x0, Float.min width x1)) blocking) in let stretches, last = List.fold_left (fun (acc, x) (a, b) -> ((if a > x then (x, a) :: acc else acc), Float.max x b)) ([], 0.) cuts in let stretches = List.rev (if width > last then (last, width) :: stretches else stretches) in let wide = List.filter (fun (a, b) -> b -. a >= min_w) stretches in let chosen = if both then wide else match List.fold_left (fun best (a, b) -> match best with Some (a', b') when b' -. a' >= b -. a -> best | _ -> Some (a, b)) None wide with | Some s -> [ s ] | None -> [] in (chosen, blocking) in (* words into a stretch w wide, as many as fit -- and whether a newline ended the line *) let fill w words = let rec go x cur = function | wd :: rest when cur = [] || x +. wd.ink <= w -> let cur = List.rev_append wd.parts cur in if wd.hard then (List.rev cur, rest, true) else go (x +. wd.total) cur rest | rest -> (List.rev cur, rest, false) in go 0. [] words in let rec go y words acc last_hard = match words with | [] -> (* the caret's last line, after a final newline or in an empty text *) List.rev (if acc = [] || last_hard then ([ ([], 0., width) ], y) :: acc else acc) | first :: _ -> ( let h = line_height (size_of (match first.parts with c :: _ -> [ c ] | [] -> [])) in match free y h with | [], blocking -> (* no room at this height: on below the nearest bottom of the boxes in the way *) let next = List.fold_left (fun m (_, _, _, y1) -> Float.min m y1) infinity blocking in go (if next = infinity || next <= y then y +. 1. else next) words acc last_hard | stretches, _ -> (* each stretch in turn, until the words or the line end *) let rec segments words hard acc = function | [] -> (List.rev acc, words, hard) | _ when words = [] || hard -> (List.rev acc, words, hard) | (a, b) :: more -> let cells, rest, hard = fill (b -. a) words in segments rest hard ((cells, a, b -. a) :: acc) more in let segs, rest, hard = segments words false [] stretches in let cells = List.concat_map (fun (c, _, _) -> c) segs in go (y +. line_height (size_of cells)) rest ((segs, y) :: acc) hard) in go 0. words [] false let layout ?(align = Left) ?(around = []) ?(both = false) ~metrics ~width r = let s = Rich.to_string r in let rec cells_from i acc = if i >= String.length s then List.rev acc else let j = Text.next_char s i in let ch = String.sub s i (j - i) in let look = Rich.style_at r i in cells_from j ({ at = i; ch; look; w = (if ch = "\n" then 0. else metrics look ch) } :: acc) in let words = words_of (cells_from 0 []) in let length = String.length s in let base = Rich.typing_style (Rich.at length r) in let size_of cells = let size = List.fold_left (fun m c -> max m c.look.Style.size) 0. cells in if size = 0. then base.Style.size else size in (* each line: its segments -- cells, where the stretch starts, its width -- and its top *) let placed = if around = [] then List.rev (snd (List.fold_left (fun (top, acc) cells -> (top +. line_height (size_of cells), ([ (cells, 0., width) ], top) :: acc)) (0., []) (lines_of ~width words))) else lines_around ~both ~width ~around ~size_of words in let lines = List.map (fun (segs, top) -> let cells = List.concat_map (fun (c, _, _) -> c) segs in let size = size_of cells in let baseline = top +. size in let first = match cells with c :: _ -> c.at | [] -> length in let stop = List.fold_left (fun _ c -> c.at + String.length c.ch) first cells in (* the last line of a paragraph: it ends with a newline, or the text ends with it *) let last = stop >= length || (match List.rev cells with c :: _ -> c.ch = "\n" | [] -> true) in let n = List.length segs in let glyphs = List.concat (List.mapi (fun i (cells, x0, w) -> let _, glyphs = List.fold_left (fun (x, gs) c -> (x +. c.w, { offset = c.at; text = c.ch; style = c.look; x; baseline; advance = c.w } :: gs)) (0., []) cells in (* only the line's last segment can be a paragraph's last *) let glyphs = aligned align ~width:w ~last:(last && i = n - 1) (List.rev glyphs) in if x0 = 0. then glyphs else List.map (fun g -> { g with x = g.x +. x0 }) glyphs) segs) in { top; height = line_height size; baseline; cells = glyphs; first; stop }) placed in { lines; length } let glyphs t = List.concat_map (fun l -> l.cells) t.lines let lines t = t.lines let height t = List.fold_left (fun _ l -> l.top +. l.height) 0. t.lines (*****************************************************************************) (* Both ways between the text and the page *) (*****************************************************************************) let caret_at t offset = let offset = max 0 (min t.length offset) in match List.find_opt (fun l -> l.first <= offset && offset < l.stop) t.lines with | Some l -> let g = List.find (fun g -> g.offset = offset) l.cells in (g.x, l.baseline, l.height) | None -> ( (* the very end of the text: after the last glyph of the last line *) match List.rev t.lines with | l :: _ -> ( match List.rev l.cells with | g :: _ when g.text <> "\n" -> (g.x +. g.advance, l.baseline, l.height) | _ -> (0., l.baseline, l.height)) | [] -> (0., 0., 0.)) let offset_at t (x, y) = let lines = t.lines in let line = match List.find_opt (fun l -> y < l.top +. l.height) lines with | Some l -> Some l | None -> ( match List.rev lines with l :: _ -> Some l | [] -> None) in match line with | None -> 0 | Some l -> ( let last_line = (match List.rev lines with z :: _ -> z == l | [] -> true) in match List.find_opt (fun g -> g.text <> "\n" && x < g.x +. (g.advance /. 2.)) l.cells with | Some g -> g.offset | None -> ( (* past the end of the line: before its newline if it has one, at the end of the text on the last line, and otherwise before the space it was broken after -- so that the caret stays on the line that was clicked *) match List.rev l.cells with | g :: _ when g.text = "\n" -> g.offset | _ when last_line -> t.length | g :: _ -> g.offset | [] -> l.first))
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>