123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270(* 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 Retained.mli *)(*****************************************************************************)(* Types *)(*****************************************************************************)typekind=|Buttonof(unit->unit)|Label|Fieldof(string->unit)|Slideroffloat*float*(float->unit)(* from, to, on change *)|Progress|Menuofstringlist*(int->unit)|Canvasof(Widget.canvas_event->unit)|Context_menuofstringlist*(intoption->unit)|Groupoftlistandt={(* mutable for a context menu, which opens where it is told *)mutablebox:Widget.box;kind:kind;(* everything below is the widget's own, which is what "retained"
means: the toolkit does not recompute it, it keeps it *)mutabletext:string;mutableenabled:bool;mutablehot:bool;mutableheld:bool;mutablefocused:bool;mutablecaret:int;(* a slider's value, a progress bar's fraction *)mutablevalue:float;(* a menu's chosen item, and whether its items are showing *)mutablechosen:int;mutableopened:bool;mutableunder:intoption;mutableshown:bool;(* a canvas's picture, the program's *)mutabledrawing:Widget.paintlist;}typeui={root:t;mutablewas_down:bool;mutablewas_rdown:bool;mutablekeys_before:stringlist}(*****************************************************************************)(* Building the tree *)(*****************************************************************************)letmakeboxkindtext={box;kind;text;enabled=true;hot=false;held=false;focused=false;caret=0;value=0.;chosen=0;opened=false;under=None;shown=true;drawing=[];}letbuttonboxsf=makebox(Buttonf)sletlabelboxs=makeboxLabelsletfieldboxsf=makebox(Fieldf)sletsliderbox~from~to_vf={(makebox(Slider(from,to_,f))"")withvalue=v}letprogressboxfraction={(makeboxProgress"")withvalue=fraction}letmenuboxitemschosenf={(makebox(Menu(items,f))"")withchosen}(* a group has no rectangle of its own: it is its children *)letgroupkids=make{Widget.x=0.;y=0.;w=0.;h=0.}(Groupkids)""letcanvasboxf=makebox(Canvasf)""letset_drawingtd=t.drawing<-d(* hidden, and nowhere, until it pops up *)letcontext_menuitemsf={(make{Widget.x=0.;y=0.;w=0.;h=0.}(Context_menu(items,f))"")withshown=false}letpopuptat=matcht.kindwith|Context_menu(items,_)->t.box<-Look.context_boxTheme.defaultatitems;t.shown<-true;t.opened<-true;t.under<-None|_->()letset_showntb=t.shown<-blettextt=t.textletset_textts=t.text<-sletset_enabledtb=t.enabled<-bletvaluet=t.valueletset_valuetv=t.value<-vletchosent=t.chosenletwindowroot={root;was_down=false;was_rdown=false;keys_before=[]}(* the widgets there are: a hidden one, or one in a hidden group, is
not *)letrecleavest=ifnott.shownthen[]elsematcht.kindwithGroupkids->List.concat_mapleaveskids|_->[t]lettakes_keyst=matcht.kindwithField_->t.enabled|_->false(*****************************************************************************)(* One frame: the callbacks fire *)(*****************************************************************************)lethandle(i:Widget.input)(ui:ui)=letpressedk=List.memki.keys&¬(List.memkui.keys_before)inletpress=i.mdown&¬ui.was_downinletrpress=i.mrdown&¬ui.was_rdowninletwidgets=leavesui.rootin(* Tab walks the tree, which here *is* the tab order: the widgets
are objects in an order, and the order they were built in is the
one the walk finds *)(ifpressed"Tab"thenletfields=List.filtertakes_keyswidgetsinletorder=ifList.mem"Shift"i.keysthenList.revfieldselsefieldsinletrecnext=function|[]->()|[last]->iflast.focusedthen(last.focused<-false;matchorderwithf::_->f.focused<-true;f.caret<-String.lengthf.text|[]->())|a::(b::_asrest)->ifa.focusedthen(a.focused<-false;b.focused<-true;b.caret<-String.lengthb.text)elsenextrestinifList.exists(funw->w.focused)orderthennextorderelsematchorderwithf::_->f.focused<-true;f.caret<-String.lengthf.text|[]->());(* a menu showing its items has the mouse, wherever it goes *)letopen_menu=List.find_opt(funw->w.opened)widgetsin(* the mouse: each widget keeps whether it is under it and whether
the press that is going on began inside it *)letclaimed=reffalseinList.iter(funw->w.hot<-w.enabled&&(Widget.containsw.boxi.mxi.my||matchw.kindwithContext_menu_->w.opened|_->false)&&(matchopen_menuwithSomem->m==w|None->true);ifpress&&w.hot&¬!claimedthen(claimed:=true;w.held<-true);ifnoti.mdown&¬i.mclickthenw.held<-false)widgets;(* a click on a field gives it the keys; one on a widget that does
not take them (a button) leaves them where they were, the Mac's
rule and the immediate toolkit's; one on nothing at all takes
them away *)(ifi.mclickthenlethit=List.find_opt(funw->w.hot&&w.held)widgetsinleton_nothing=not(List.exists(funw->w.held)widgets)inList.iter(funw->letnow=matchhitwith|Somehwhentakes_keysh->h==w|Some_->w.focused|None->ifon_nothingthenfalseelsew.focusedin(* claude: and the caret where the click landed, as the other
* three put it (found by examples/gui4/tests/Unit_gui4) *)ifnowthenw.caret<-Text.byte_of_columnw.text(Look.field_column_atTheme.defaultw.boxw.text~caret:w.careti.mx);w.focused<-now)widgets);(* and then the callbacks, which is the whole of this architecture *)(* the default theme's knob, for where a slider's value is: [handle]
has no theme, the one thing it would need one for *)letth=Theme.defaultinList.iter(funw->(matchw.kindwith(* a slider follows the mouse while the press that began on it
lasts *)|Slider(from,to_,f)whenw.held&&i.mdown->(matchLook.slider_valuethw.box~from~to_i.mxwith|Somev->w.value<-v;fv|None->())(* a menu: a click on it opens or closes it; while it is open, a
click on an item chooses it, and a click anywhere closes it *)|Menu(items,f)->letwas_open=w.openedinletunder=ifnotwas_openthenNoneelseList.find_opt(funk->Widget.contains(Look.menu_itemthw.boxk)i.mxi.my)(List.init(List.lengthitems)Fun.id)inw.under<-under;(matchunderwith|Somekwheni.mclick->w.chosen<-k;fk|_->());ifi.mclick&&w.hot&&w.heldthenw.opened<-notwas_openelseifwas_open&&i.mclickthenw.opened<-false(* a context menu: the next click closes it, on an item or not *)|Context_menu(items,f)whenw.opened->letunder=List.find_opt(funk->Widget.contains(Look.menu_itemthw.boxk)i.mxi.my)(List.init(List.lengthitems)Fun.id)inw.under<-under;ifi.mclickthen(w.opened<-false;w.shown<-false;funder)|Canvasf->letat=(i.mx,i.my)inifw.hotthenf(Widget.Hoverat);ifpress&&w.heldthenf(Widget.Pressat);ifrpress&&w.hotthenf(Widget.Right_pressat)|_->());ifi.mclick&&w.hot&&w.heldthen(w.held<-false;matchw.kindwithButtonf->f()|_->());ifw.focusedthenmatchw.kindwith|Fieldon_change->lettext,caret=Text.edit~typed:i.typed~pressedw.textw.caretinw.caret<-caret;iftext<>w.textthen(w.text<-text;on_changetext)|_->())widgets;(* a press that is over is over, wherever the mouse was let go *)ifnoti.mdownthenList.iter(funw->w.held<-false)widgets;ui.was_down<-i.mdown;ui.was_rdown<-i.mrdown;ui.keys_before<-i.keys(*****************************************************************************)(* Drawing *)(*****************************************************************************)letpaint(th:Theme.t)(ui:ui)=(* a context menu over everything, wherever it is in the tree *)letwidgets=leavesui.rootinletmenus,others=List.partition(funw->matchw.kindwithContext_menu_->true|_->false)widgetsinothers@menus|>List.concat_map(funw->matchw.kindwith|Button_->Look.buttonthw.boxw.text~hot:w.hot~held:w.held~enabled:w.enabled|Label->Look.labelthw.boxw.text|Field_->Look.fieldthw.boxw.text~caret:(ifw.focusedthenSomew.caretelseNone)~enabled:w.enabled|Slider(from,to_,_)->letfraction=ifto_=fromthen0.elsemax0.(min1.((w.value-.from)/.(to_-.from)))inLook.sliderthw.box~fraction~hot:w.hot~held:w.held|Progress->Look.progressthw.boxw.value|Menu(items,_)->letlabel=matchList.nth_optitemsw.chosenwithSomes->s|None->""inLook.menu_closedthw.boxlabel~hot:w.hot~held:w.held@ifw.openedthenLook.menu_itemsthw.boxitems~under:w.underelse[]|Canvas_->w.drawing|Context_menu(items,_)->Look.menu_itemsthw.boxitems~under:w.under|Group_->[])