123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189(* 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 Layout.mli *)(*****************************************************************************)(* Types *)(*****************************************************************************)typeconstraints={min_w:float;max_w:float;min_h:float;max_h:float}letloosewh={min_w=0.;max_w=w;min_h=0.;max_h=h}lettightwh={min_w=w;max_w=w;min_h=h;max_h=h}type'at=|Leafof'a*(float*float)|Spaceoffloat|Spacer|Expandof'at|Stretchof'at|Padoffloat*'at|Centerof'at|Rowoffloat*'atlist|Columnoffloat*'atlistletleafkeysize=Leaf(key,size)letspacen=Spacenletspacer=Spacerletexpandt=Expandtletstretcht=Stretchtletpadnt=Pad(n,t)letcentert=Centertletrow?(gap=0.)kids=Row(gap,kids)letcolumn?(gap=0.)kids=Column(gap,kids)(*****************************************************************************)(* Helpers *)(*****************************************************************************)(* A column is a row turned on its side, so everything below is
* written once, along an axis: [main] is the direction it runs in,
* [cross] the other one. *)typeaxis=Horizontal|Verticalletmainaxis(w,h)=matchaxiswithHorizontal->w|Vertical->hletcrossaxis(w,h)=matchaxiswithHorizontal->h|Vertical->wletof_axisaxis~main~cross=matchaxiswithHorizontal->(main,cross)|Vertical->(cross,main)letclampc(w,h)=(maxc.min_w(minc.max_ww),maxc.min_h(minc.max_hh))(* the two flags look through each other: [expand (stretch x)] and
* [stretch (expand x)] are both a child that fills the room along the
* axis *and* across it -- a pane in a window, which is the commonest
* thing a layout holds *)letrecis_flexible=functionSpacer|Expand_->true|Stretcht->is_flexiblet|_->falseletrecis_stretch=functionStretch_->true|Expandt->is_stretcht|_->false(* [share] along the axis, whatever it is given across it *)letalongaxissharec=matchaxiswith|Horizontal->{cwithmin_w=share;max_w=share;min_h=0.}|Vertical->{cwithmin_h=share;max_h=share;min_w=0.}(*****************************************************************************)(* Pass one: constraints down, sizes up *)(*****************************************************************************)(* Pass one. The flex rule, which is the whole of rows and columns:
* measure the children that know their size, then share what is left
* over between the flexible ones. *)letrecchild_sizesaxiscgapkids=letroom=mainaxis(c.max_w,c.max_h)inletgaps=gap*.float_of_int(max0(List.lengthkids-1))inletloose_c={cwithmin_w=0.;min_h=0.}inletknown=kids|>List.map(funk->ifis_flexiblekthenNoneelsematchkwith|Spacen->Some(of_axisaxis~main:n~cross:0.)|k->Some(measureloose_ck))inletused=List.fold_left(funacc->functionSomes->acc+.mainaxiss|None->acc)0.knowninletflexible=List.length(List.filteris_flexiblekids)inletshare=ifflexible=0then0.elsemax0.(room-.gaps-.used)/.float_of_intflexibleinList.map2(funkknown->match(known,k)with|Somes,_->s|None,Spacer->of_axisaxis~main:share~cross:0.(* an expanded child is measured again, now that it knows how
* much it got: constraints down, once more *)|None,k->measure(alongaxissharec)k)kidsknownandmeasure_lineaxiscgapkids=letsizes=child_sizesaxiscgapkidsinletgaps=gap*.float_of_int(max0(List.lengthkids-1))inletextent=List.fold_left(funaccs->acc+.mainaxiss)gapssizesinletthickness=List.fold_left(funaccs->maxacc(crossaxiss))0.sizesin(* a line with a flexible child takes all the room it was offered:
* that is what makes [spacer] push things apart *)letextent=ifList.existsis_flexiblekidsthenmainaxis(c.max_w,c.max_h)elseextentinclampc(of_axisaxis~main:extent~cross:thickness)andmeasurect=matchtwith|Leaf(_,size)->clampcsize(* both mean something only inside a row or a column, which handles
* them itself (child_sizes): alone, they are nothing *)|Space_|Spacer->clampc(0.,0.)|Expandinner|Stretchinner->measurecinner|Pad(n,inner)->letinner_c={min_w=max0.(c.min_w-.(2.*.n));max_w=max0.(c.max_w-.(2.*.n));min_h=max0.(c.min_h-.(2.*.n));max_h=max0.(c.max_h-.(2.*.n));}inletw,h=measureinner_cinnerinclampc(w+.(2.*.n),h+.(2.*.n))|Centerinner->measurecinner|Row(gap,kids)->measure_lineHorizontalcgapkids|Column(gap,kids)->measure_lineVerticalcgapkids(*****************************************************************************)(* Pass two: the parent positions *)(*****************************************************************************)(* Pass two: each parent hands its children the rectangle it decided
* for them, and only the leaves come back. *)letrecarrange(b:Widget.box)t=matchtwith|Leaf(key,_)->[(key,b)]|Space_|Spacer->[]|Expandinner|Stretchinner->arrangebinner|Pad(n,inner)->arrange(Widget.insetnb)inner|Centerinner->letw,h=measure(looseb.wb.h)innerinarrange{bwithw;h}inner|Row(gap,kids)->arrange_lineHorizontalbgapkids|Column(gap,kids)->arrange_lineVerticalbgapkidsandarrange_lineaxis(b:Widget.box)gapkids=letsizes=child_sizesaxis(looseb.wb.h)gapkidsin(* a row walks right from its left edge, a column *down* from its
* top: y is up in the playground, so a column's steps are negative *)letstart,step=matchaxiswith|Horizontal->(Widget.leftb,1.)|Vertical->(Widget.topb,-1.)inlet_,places=List.fold_left2(fun(pos,acc)ksize->letextent=mainaxissizein(* across the axis, a child keeps the size it asked for and is
* centered -- unless it was stretched, and then it fills *)letthickness=ifis_stretchkthencrossaxis(b.w,b.h)elsecrossaxissizeinletcenter_of_child=pos+.(step*.extent/.2.)inletw,h=of_axisaxis~main:extent~cross:thicknessinletchild=matchaxiswith|Horizontal->{Widget.x=center_of_child;y=b.y;w;h}|Vertical->{Widget.x=b.x;y=center_of_child;w;h}in(pos+.(step*.(extent+.gap)),acc@arrangechildk))(start,[])kidssizesinplaces