From 0846fe22fc07358eeb5d975b6b71182cee791075 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 8 Mar 2020 22:07:46 -0700 Subject: [PATCH] Place Strategy for Frame Widget --- eg/frame-place/README.md | 29 +++++++ eg/frame-place/main.go | 144 ++++++++++++++++++++++++++++++++++ eg/frame-place/screenshot.png | Bin 0 -> 23536 bytes frame.go | 6 +- frame_place.go | 97 +++++++++++++++++++++++ go.mod | 9 +++ go.sum | 7 ++ image.go | 49 +++++++++++- main_window.go | 40 +++++++++- 9 files changed, 374 insertions(+), 7 deletions(-) create mode 100644 eg/frame-place/README.md create mode 100644 eg/frame-place/main.go create mode 100644 eg/frame-place/screenshot.png create mode 100644 frame_place.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/eg/frame-place/README.md b/eg/frame-place/README.md new file mode 100644 index 0000000..35060d2 --- /dev/null +++ b/eg/frame-place/README.md @@ -0,0 +1,29 @@ +# Frame Placement Example + +![Screenshot](screenshot.png) + +## About + +This demonstrates using the Place() method of the MainWindow and Frame to +position widgets around the window. The MainWindow itself and two child frames +(red and blue) are given the same set of buttons, placed relative to their own +parent widget. + +The options for frame placing are: + +* **Point:** Absolute X,Y coordinate relative to parent +* **Side:** binding the widget relative to a side of its parent. + * Top, Bottom, Left and Right to anchor to a side. In Bottom and Right, the + child widget's size is taken into account, so the right edge of the widget + would be `Right` pixels from the parent's right edge. + * Center and Middle options allow to anchor it to the center horizontally or + middle vertically. + +Click any button and the title bar will update to show the name of the +button clicked and which parent it belonged to. + +## Run it + +``` +go run main.go +``` diff --git a/eg/frame-place/main.go b/eg/frame-place/main.go new file mode 100644 index 0000000..8cf63c6 --- /dev/null +++ b/eg/frame-place/main.go @@ -0,0 +1,144 @@ +// Example script for using the Place strategy of ui.Frame. +package main + +import ( + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" +) + +func main() { + mw, err := ui.NewMainWindow("Frame Placement Demo | Click a Button", 800, 600) + if err != nil { + panic(err) + } + + mw.SetBackground(render.White) + + // Create a sub-frame with its own buttons packed within. + frame := ui.NewFrame("Blue Frame") + frame.Configure(ui.Config{ + Width: 300, + Height: 150, + Background: render.DarkBlue, + BorderSize: 1, + BorderStyle: ui.BorderSunken, + }) + mw.Place(frame, ui.Place{ + Point: render.NewPoint(80, 80), + }) + + // Create another frame that attaches itself to the bottom right + // of the window. + frame2 := ui.NewFrame("Red Frame") + frame2.Configure(ui.Config{ + Width: 300, + Height: 150, + Background: render.DarkRed, + }) + mw.Place(frame2, ui.Place{ + Right: 80, + Bottom: 80, + }) + + // Draw rings of buttons around various widgets. The buttons say things + // like "Top Left", "Top Center", "Left Middle", "Center" etc. encompassing + // all 9 side placement options. + CreateButtons(mw, frame) + CreateButtons(mw, frame2) + CreateButtons(mw, mw.Frame()) + + mw.MainLoop() +} + +// CreateButtons creates a set of Placed buttons around all the edges and +// center of the parent frame. +func CreateButtons(window *ui.MainWindow, parent *ui.Frame) { + // Draw buttons around the edges of the window. + buttons := []struct { + Label string + Place ui.Place + }{ + { + Label: "Top Left", + Place: ui.Place{ + Point: render.NewPoint(12, 12), + }, + }, + { + Label: "Top Middle", + Place: ui.Place{ + Top: 12, + Center: true, + }, + }, + { + Label: "Top Right", + Place: ui.Place{ + Top: 12, + Right: 12, + }, + }, + { + Label: "Left Middle", + Place: ui.Place{ + Left: 12, + Middle: true, + }, + }, + { + Label: "Center", + Place: ui.Place{ + Center: true, + Middle: true, + }, + }, + { + Label: "Right Middle", + Place: ui.Place{ + Right: 12, + Middle: true, + }, + }, + { + Label: "Bottom Left", + Place: ui.Place{ + Left: 12, + Bottom: 12, + }, + }, + { + Label: "Bottom Center", + Place: ui.Place{ + Bottom: 12, + Center: true, + }, + }, + { + Label: "Bottom Right", + Place: ui.Place{ + Bottom: 12, + Right: 12, + }, + }, + } + for _, setting := range buttons { + setting := setting + + button := ui.NewButton(setting.Label, ui.NewLabel(ui.Label{ + Text: setting.Label, + Font: render.Text{ + FontFilename: "../DejaVuSans.ttf", + Size: 12, + Color: render.Black, + }, + })) + + // When clicked, change the window title to ID this button. + button.Handle(ui.Click, func(p render.Point) { + window.SetTitle(parent.Name + ": " + setting.Label) + }) + + parent.Place(button, setting.Place) + window.Add(button) + } +} diff --git a/eg/frame-place/screenshot.png b/eg/frame-place/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..35a2ad0139eb8a69a7b2bbadd87fedb239b56454 GIT binary patch literal 23536 zcmeFZ1yq##+BZCif+9#LT?&Xwcb9;KBB&spl1g_B11M5bqEb={0wUcFA_5`^(%sT6 z=@9c>v-jz<&vTyd`Of#Q^}g$MvC%yYbKi5{|NnLU>Y9){s)|GeGz16)f=F3O{vHBx zf*t-7IE@F7jB-7+fWJ;U%PMP}hCkk?&4Lk#iwI@;>lz-3D`QA)b<0%lWBb(LU=8`a zBE9wCbpEm8Y}58TG6mD}!Hzj;-4BIh#d^k^OCF4voZMqbL_6>oNk~PS<<=~t2CBzP z>vUq}Payc`o-WA9%geLGCHNi{o2`V2UJ|pPxUyz0 zpi`W2G7V{4F>tg{_*IZS$3tQ&f>W>TO=WFc>ZdD}kxkX&?y0MWEvEM);5FzP4@1sB zpIi>uBx?*6Qmc$;RL`3!`0UK&w3M*%5SiIzo1CQ31zMo#2*o(0_J8BpFS=e{_WX;^XEz22i z`x^OY1BW%6_1~==jd>6FzVjC!7WmAb2qhp+7HUfUb!PBzd15l;N;1_38uN8LvT9gY zy!VU5(ehYVS42JP*JUXQ^=H-L;chwcap=7KwKY2z7nezUqj80%^jj6q^VZ@69e1p> zMUG95PnfO#Vq~3<7ea~o{PY${_FlRcMk{=7Z{2-5p~~LjhXYqtb+uwaSo++dp)E8W_Or z)OvPfXR8I%{`JDKpE+l}_ffscQm@S62JmNVLUfZX|lFX$qu|yHN$p&-uA;oG2YLR;dmIJCaI|&*pi6W$k@8jce z8$D?)Rioder=LPRuR{f07abcLv$D5tJi_1OHGd|Ika0fBzxqY`Szcc9!3);gub*D- zYU4pQ6IN7&HBQtWlBH*=zIKg~&+eO6!@O1a@q%)4cJ`FCw6FKkju!45?T2wSiOrKt zM%vG)`7ItKX0zAr*N|D>QcKIuw*G=1O8s^;;H#xYX*XW+ct#Q97evfBU2i{3XuqCZ zG-K#<3gNJ}Lhb$2ap=wRa)UqqKHSDBp>d+NXm*m~2bc3Ou9OUtUS@U&3%33&!{f5ke%sBXX>F8wa~L&l-`D7k z&COGY_daC=EyoiWxjp9jgNv7=Z@%q&$yZaxTCkI{;Pj+=L_R;QIbn)1JPaDZ0Vvf6QLl=WbN2BZ7m$JEjs%0mI;JBqD z88^nIovJ-yPe3!@An45b(WtF@Oy495<5+ou(WoXEQ+JIz`Q??g9ZmM#H6KdrX`jcQ zYC)6hm?_c2Md@BDaW@V_Wu+$rudaz4UO%U+t6OkfIzXCI!Z38OtGn9s#dv2e*S|cH z_~tFu>0)+FKzy;qOlxNHr`U4)1OsWkl3&;d`7>`8u&z;_el1S9qf^&<|gJinkI{0uz2)ZGsH?DC$brc;=>eKuCLryTlr~jqef{~)+ys% zoZbOn)4NHTopFyhXIl1p>#K%S_jYM=^6jkz(7#4UM>mR7Fm#6M8Iz6t=?eY?#>XB! z3QW-t(CzUEuM=9n8M~Gl=;N1>ZDDnG%F=Pe<6peVLQb~%eE3-MuxBS<@|U}sENL-a z(~TRN(yEsaXSl=alD8U6vbkEQLgb&kZ<-SE-898+CQ2@!u!4EtC-M9lBR26;T&9G_ zhEsxPB3;B@m5kyd#Xc!A)NJ6;c0w1EH(RY@enRHu>t4R?3pjN2&@jx){FDwurC;RSm7RjY>&@eumC zhlG3U6N(15ovK62)juS!SaD@|AZN1ckBD9`4>*<=k`Ck>_`?-V_L;+%>o;JFe$XIG zjd+ulb!Ng_U3lTJc}WtfN>YRJc7W$DwUK*lOyYbvK0IY-B|y>FNnv{&*+yRBi;+6s zowfDn3(T&qHNaqC<=A$23^_l6-5g-QwDhL(SW7@yIBh93n~L9pF!^9Jk~6Qk8rex5 zwzjs`a$F*ymI5U?^s#kYT zW{WxZcs(P}&MN$y84iEi?auh(||P4~@;&sf#)~lB)|#b(88i5F6Vo${*FFc6$BV2-PzP zeR{S#4G%?qcBebLy1E{}USjk1$mm@-Vw3Ab{`}F3@ayk?oVmuCmBKkZ+BI%+C*$*H z6m`r}tqacf=$peIFV*4=t`v{_8uMCR*!Q7bB2unh{zz6k8%8fqk(ZYjNv@48HyJAN zPw>yEjEwF(QOGL*%DCG+( zti)Cyupm_TnA6IPc?5SDq zjLS_+HH>{pxJ@p$aoKtgmuPL(2xIYmvD$r=?(OTrWYtVLb#?xl8dlR`bv0}kMHv@H ze~rM$Gf6MnlJXI1I^R}bt!ADh|HL^rP|XOnJBH8U9PcX7v*@L|<>rdA?y3hA!;?MO zAIB>>K!B8TRlfL=v%7w`A`i1evnC`oo%OJX7jOU;yx^#3%4X??WMy+uNx( zlrxpXh21GvrW66?iunh=^v8CcV%vMt*3m;lLtED>_G})DHb*i>TKeqE$=4NJh-8rb z)aomZvzn$TWNqoJe*Wpye?w&F~=H@ zVQcc(jf(95I*{8brVOgt?$*1ajv5>IIcvGGyV-Cn|1&Koa&YE4)Afy-%uCTX zyT-=C1JQUA-uoo<^pZznaGZTK+N5K&5_kO3hxPDUyH%fyOUo0Xx1K`{{Ne3DHP8KO zNZ>u&M}UVQR5^MNC^Jtua2CkB=_?q-`uMkJu97aTis!f59S4DpI` zJEac@do0^O*JR3VEcXgzzZ&Z63m8q@BP|u6kq!@O+TWbHYlI;>-iq}7tmS)4>*BpsTo(&h76#nc3LzIKgkx@ws!L_NlLU;#u1i zT1*f>gBQ1nl*b0b_u%I(Z|_Zs;Sr?U`=)>^1uBJf(nge)Kd&hQv|Dn8l^H*$VyPVtn{etpYPy_iIS*gbI~3 zPW9fQnv09bdhv>UM@J%e05j8TX~z=)Z+XT)UmgJ1>$J2!EiKM*use**4WCQWSzPe& z+n!j(c~g8f2SMgDKmbJLDv*UM$4;0_sdWdJif ziKLJv-S1DK&Yf`)F^!6S45zGQ)hJfnot=@-q30}lXj@xnr=pV5>2Y5SN-6!`=jP@I z19=8ABN6k-~aTr!g1z}X7-M+qN1Yd!S1Sdf$?L( zXXVd^hjqWqIw_^6&6$yfKVX(*%~VT?c=3WP$EcnG;c&2P3yr<8sp*Eanu*EV+cCF) zyjSo_+zm{zult3kS8jL1%}uDgyBiTKEv5M5%Tb?}kx649QE*t;=huSP8(oTwi<#Fh z5d4^%d+_bGpvP1IWr6d&s*9_uWWUqv*RP+)!~~|_ij_^jH}xfo8R2(3L1=2aDRi*V z6yJ=Gy3A(iNnjuz_ENLHyd*6>!M4icbfKT&B|*T6Ox2`r-=I&eFRzfEKX1yTHgQWB zu9EzN2TZA{sm~J=!{+8J{5^wSkH=;?s{L57ab4No+LF`Oj%!Fytea6!>!J#)w;d@X zs!+++%r3MW6O&g^c(B}`i#8uw+JCfFklOcXvtQ5NWNozaDjS=ulM`Q={X~TIK;GiK zsQVh-?%G)B=%~KN{rgiJQ;ihdikUr^E?+)nXlN+nb11UAIui8c$%$97vCRDZ=S@sZ zly2V+(9Km)P{8x{_J02IC2py1INU?M;#PUt;-)x4Lc*u@_0m#D+v--Oy02nlWZ&M2 z)AzY~lK`&5vuDrVXs|iXwNn>o?gXnA3knKWAosWok*k7OipLjEc=ztz ztDKyDoiF3!g0s{!nJ$9*qO7c-)qW?>s6GPj?|I3Zh_{yc-wt@%#9Vo~{X zotmb6tgLuf)RN_I+&E?N?e!@mBcqpX_3rBohNzwU71hGJqObjS1ip# z9bYFWpNm()OTTx9l9JM9w4!UlX=G$1DlYD{ySw|-fBC|3ytTud@Wd;-_U$y_3IO-lWG}FCQy+}mo5cIMk;D*Qc!T| zTk*Jg#g6zMev9L6T&WtFhGoj^=HS3nQ&Z!xF?qeDyj(6-NlN-P|_SVMU=jAEb*l=LCO>eJ8dy=@8v>MzQA=}}n zu|_`B&hs5j?D;i(R`In1V_ulrtqyUlL{(}{2}Od0k}}A3xqoV@FPl}ITS|%nu<=uU zuK6Arfe(WZRCKEj-6v3#coWoc7~2ib8~_Bvo$XX3lB{0 z@R~Lo&vztU<>n5$mnDDwIu5iPydy++3TH(A3u$H1T4}wA3hK~ ze*Ac_7Fpg9Ub|}?-6oUjdprNK$JaQz=;Dahsp&Y@sIWQ5ufO%*A0>gVfYJwOLX-&U zUdm6;h?I^(n@iTbOm?<~C2C(pm64bm7u#?D`;GK}*nj_z{57HeadHO=vQ`VE*5lwW z9AaY(@Q*J{gzTE+HtPf=q(p2YaR=9&Bx(q;U`X~qat-!JWZZVxky;-E&ka|f(41~Z z3FxQpZd$GL-#rLe*yPO_)VXylv^Lw=H90wXH}A=PQBy{u*aeh??t#j-&m}MYKb_1> zJo)wB-rm7#g2p*QTdJ_)@=m&lvU7B~b&=L3B`+1Y{{&=sbDj@Dck_Sx^dta)AG5PV z)nWoqb8KAX7(UYZ@7Ba)V^1Iow}2w;w}KPanQIQf}JtPm-FvdLJb;U^o2UziidRulu`qv)gQKLiD5*(lPse`$*Ia^qf$Op+yk}-kP9ns? zGb<5=`MWJRsGUBo!2%OpCT8YBi*GdE+g*z2hk!dOD=W2&Er<*~rcSMlR-QL7Fo-No zgy!-tJ>6unJN-_UhKSRT(+K09jN26+n@nyigE;XjfmlWV?OU8PXU;rtHT%-pS$Fu4#-YoU+i!KPimsix7SXrBmlj~yb?8a+CK7W>n){1Sv-@bjr+d3y$%dI~0 zxVW&d;^$w13@W|LiV*!^q|6qVYbdMlQI1~uxu~e9!7A5mpCLM=01a=B2qITAw0zv_ zEY#Q=ckhzEc=@srb>OC7?RHzb6LggV_jN;nXQ@zsKwd5L5QYAE?b@}CpFckV6`(`V1hJ$l5}`$&+Fk8g5j#y>HU?mZmUuy*+MZ8wp!g|v?!W%dsa zW_r}512nU5JbVb)FQ9cc^W8f@aBttgKYcTu%(5eq7aEAaEq=%6n3)gINX^a76}Wr0 zcLVgf6yXdt^YlZCEj!P}<)@~efTN0dZC@3jAyQ3|B0|7>VuSbO_xd%>Xf2X5zjl{~ zRWTl!V0Z-8+^v@%p#=^*;3Fc4Jr!Cm|uhV>>L6V^B>o z-#cIRGmFbb`0S#l44~NKiF5n<`W|6$Pn`HZIB2!$1kB-ke}CZj?{^TA zDFv=#^mCml(qY4q=gOASzI;)l6LCP?zI_|k80);Xm6cz9etyG|Jis6ewRP(f8~n0` zVqh2Yii&A)GZTdEn*kACeA!Ak$6L+~+Wb3AeRejxuAbgEd%CEXCfbgO@$vEZ`T2_O z?&Ur$=zk4lq?^boDdm`=2t_egwO##GdOJWqXl3Q(?rLfZEvR!!NlC%W1ZigX?cK7q z<+SKXeAvHThc|LAaj~t%rqA-9fJ^}cGIpe%+oCj8F5luX>;QB!wZ0w+^GdS^V<1>R0w7@A;BlUYvk&lm*tgP%t>y?zr^jJe#8=IBq;^hDhICM)+gYwJkfjUC(K>w?7 zTcts)UCo{P6mVAQ=1o}-50MydqX^&yqjf0c;Oy(3&Iqn?JqooyX92jl&OyC}ueKOY zIbGe?4MY|Fqf(+^S2q<*pL}F(O?u(N$*kU@wL~eD7_7x@--$XDEwoJsN5`&uO>OO9 z&#gI~+`P2gCMK7F#-9U`=KK}ai`?AAw_>@P3Qb#93lP z;5>i*`o-F&arJ_T>(T{KWD%3Vu7X2Cc;hnHuY88W%mlpxwz-n3s)Dm~aYK#VZQ)GG zjvrHvL0>vLnrZE89uE#`dw6;_^z;CQAobY#CX7Q^$8dq*)X$$k3;rrrEG#XZK)gJX z(C~!TtpGP7H7!li;kI^AN(v)DG(xV~?J+4qn(&6*a?m<(V)c3-mEKtHp^BhQjcO^F zM!>n2onJ=)q63Fv4OJ)w zCm#~6RI11V1mD5gx$)_l^I@vOmXa)&FE?~{5<`zXO-$SZB~gW|bMz#KgtJ@hF`88{a9v+{U=Oy56w1&TgHJS5J?{}&?C$RAAq7zU;RBn^cn!5yo<7_S{yu zD=!HMTmTe?1uvp0k&(np%gbOs@5~PL_dl3xkGHrjTrSzMJ*x?gU}IzBBfv(Gr0`YB zON$A4F_x3^ClnFyoiZ#TqiA&RO1LkXunS(5hn`1s4$ud>e0{NNzp z(bu>68S(;jJh`;41$zYDO1^xHu6Bb9W9WP2iY4clFJA_I_xi0{XSob(g7?Oe_JhlBeWfDA(N$%T2SzPGUUbEj6LqQ7rP=wfVUn%jlxQRnmO8! zq4s(2PX&2wHcWl?X%!P6M?mcidD?EIcP&4vTqwzF_E?Y zS#=i;fx`EusE8~nDe3C9YuC-qFaL0=tj)e5ulbSI=F}y0rb7|kfr}ZZb^(H#ni}*l zS!3giZtLT#&()+!yJVo#vb<|KgM?;HtLq5e1B8H1Fh82 zUZwsXcd-B?~g_vkA~1^4h*4RDyUO|=Wz z*K#<8N9lP7Dp6GHAF{K9yuZbu`+bLs%$084;;)bftgDkN#XKg& zgoMKd^RMLG`hyNm)8!?O7Q^>DOUZTd+O;!NQ&ZY?-l70{9l<^@iM5U_dzYDsl^lU? z#euTub1*UyvG6k}yZo5Zdjkh*NK{f%Zpxqf?@*zed_<_p4W5X)VjsHF($doe1Wz;` zrF`p(8u0wreYw;mp2y^b54tC18jv%3pbovnZZlYb1CSfb!==y*%!rRMoO<8CfA_;G z8D@Mx=I3cqwgr5C$+x)cF-Pl#ZmYTgaEw9c1Q|m~MMWL}2e4p}drdTtjt&n=pjMN3 zA0+E6atZ_LZEbBGEVE6xUVwuDH~_-qB|bhftka>T72CQ?CMq6Ceq9vZQC0u%Sii*p zePLSe7PMw2adBwsn|KdQO@ZQ^0-OdXH2F<<;-s-x?H-pnxJaqE(|2a+rM#&e9UVcx zWzaMlt@p*Um;DA}9|Qt)VvptAavq=?7s$!6s=!FOeM_nJ!1Jgm97aaQA3uJ`l>iTI%1Adi6V}la?qfH|K=dXUjTvN)vF62 z0>Mk0%zlj)LGG^j{er&vJU%`oO)*k7t!on06ohe!t$as>8t$2|y&YoEF`z)~Pls}2 zv5&;BA1}Xr`62`B&24o!eL^(Pu-5CNwSP1gnoS-gxdKb*&o_z$A+!d>T?l)A9I>wt zhhj8IS-&4}!xpkj03NPG$%QNX>dlYTfm3|W~s z>1>k*dVAmG<`UhC;ou!kZs zX3U;|9*Ko|2#@KID;wj;^~Djdy)}cH-C=8Nz6lH$D57_Cbw3zI%JF7~feZt3KQ1B} zwX1XT)TzbH8}jmmLm72Dn65~lr+^A*autzCakBI0!9vG^rW>IYL~-1(UwnLg5PlmQ z4x<&0jX7>)qRh!Uq$*xsVsHcsZo{zd?t3-I=w$?y%=p_^bLdijRmkoL_}brJk8JNZjLxQ z-ntL9o`!~|ooghK?>E5!1EwL~;SpF|mzI5h{WQl4d)SBa^eg#{lfj$AqALAe!qcaz zc#I~@tE;NcRoXCFHuJMTQT|;W+yEPLx;yzU!D7~$% zE!^5u-?z>fWcHXGAO50=;X~o4oR>r)RwR_SR%gV}L+k}eF{@UM< z^RB<#98Mbqst-U${1ntK^$A?uyGBNlkkB})Iz%t$@3ir|)vIF$YHDloIU98$;0Nr4 z1iCqB1|0eoBxE6GGLj{_xpfgxj3CbvIMh}!aAY9A`-#)=PObPds9!V1ol^XRUAr_L z9mCqxW!(p@wIGCn(@^34iw{&VFqVv6e2}Zb^tMU(`1p;Djkq9M@QaOg?~EH^$_UFP zZh_T_A z017<|4ZST@_#hu!gPb8&gI#Qc^}f=;kG^+zKZ%K<(68|zU9a6sZd$IbKenZ|^PF3# zWuc^|4gt8s1Y)T3T>E1UwkBZicMS}}fsw3?Ra0hVWyOiRb0IGN#fKS~ARNV%zWrJt zBrGhPqgzS<5-DDOBsD*txcizsunC|71Oyl`hca2O z5)lY7lv zszIUR8Gc>2vt9}>d$=Wy;Rlzg0F=(d%}C#q2no#LmdDY~z|`WR=p^3A@y=T9lpws_ z{{234OWk>d!||af9aL(Xa6AzF_OW%q==f(WSfs;mmTNIZ&Q4BuU?IX5d%0Ttu4X*p z?(Mw<6QeK-T46hR4UG!`4L7yd8NCdAxkW^%`ua>FOX*Uhg!mVz5nwQwh8M^->l86F z&EX{w+I3p5z!(Qa**o{| z2Y?2xWFenl?>LBwC3TG7zew{cEy5&%vxriWo}bBcbn{@)>L^0X!=HSx2T0G@YFlc0!6MgAqY7%-k%_puZL777 z?pkQH?e^~1){-dxEjLq))CQJyj*N`#Vr*MfY^?Buwr@b1+}z5BdhWB41E^C>eIze0 z@b&5i0xJPPk8*>iX?>q={#R-YQDkUnW9p4itjyy-FpzIV47xc)8gPt1UM%VjI7|5) zb`*3h4x>6c*yk0fgWZBQK}-GfqOXC<0JMTH$gU7Rgy^a8r0y1#z;I-^n{@F#@UJ0? zqLdzU-yFTY2)v>T1Hb#{qQ#(V+Dz2ZgN$-THBrXUkRgW4;F)Pl z#8CAk`A;|1)Pi(#oADXF{m^JH|KfAIB6y<9g^utOb@lU*GbwOcyazr$ov71EM8U%z zR%{IucU$pOVQu_J5SYNB{=2<`wk%UimI#T9qw@DeXSDn){Y5@v>E8%2V<4S^Pi8V! z<%$is03Pk?>pO*DZQ1!d=++>3ecR*kcUnS#*UV7Wab6v{h>h7s-UDiDacBq&G@Iyk^A$cJB7RXqnSM>F5h zwrKQ^Y?dz1Qe94vb3l!{qoHvUBxHWejyTD&(OnYJ4?r+VN=qR_ds!hfp?FM7L*pVO zQxqc^0u<637JD*rJ}y-JRe|}T%|fP(LPk%l&;4*#TE?!I2XLJ5tKg)i70-Ml;7{;yvRBO|o%+JHL(LF>>&C*$`e|&89&$Gdk-RZsZyQ8r3cJ`QiuvNIs>I+jVAT#8HFWo)OPsXt zF`Osd%*VL+-+OycM&C4ds{0eJ8Kpc=LSnqTU1u}Iz=Vt!cu2N3Q6JgSp_=x%DRI>Y zG#NAo5Kn4pY2S~Z?bkY6S}X@wnTksPg$J`oW!g$b6UNU#Ys;YNrT*Zza7easYj_!G z0sw(-=;%;mebtV6FxFu8LlOw}3kyv|oilWLzS$Djvjo%K2Y?JFmc!bp=+)@2I6a*7 zEk`?y1C5-KN-X~*!4w!Ekw{v}=5}mg#5`=*H0>KV?zcZwV9xme4ng9MOch~!0|Uru zNa{E`g^zDK{W(O$Wr0LYOzaL&iMCko1WA<0>LsU>5Lm{x$wFU@&#PCjurnf@oSazG z6F_yXiT&PDi(m_>C^Lwy&CSg?dk;?nLK+)0@C~bUS$xrI8g?!1Xwa&!pc}cGw>Kfa zUV{q;3$*vQkWB{?q+M=D4Ra;o(HE}qN@ID2!gdL2;u39#S-0s`>V zs0e=xe3S%zxtpV3Nv2n3(}49ZK?(WX)HKydEd4o@yG~?fwGLA;C&FY2cIUrIgbw1EcAR)RSN)6iUUy-1?y9SF}yF?*~O zd`cJ%rlW1)iJGO_SoQzS5o!PEXD{td1I_j?In4j0CGnrPTd;WxXhHQiS$6YIkVfhF zWc3VH6R1A6{DXPA|EYfDOB5A^uyw!qMWdt`yDv-+Yt+6X!r7s+pYCqNi!OxQf~9Gc=s57pOb`5@0*7z6NO-dNxh} z1nZxajO@wYgfAnEg<)+-h)7{uYqVF$qjv{$@K3Dxn4y~V!58bcRCw*Mfd+sj;jj&1 zg=HG;MOwc*Sb=hh@UnA3tyRu%?2gzk+k2H-7=_31&MQ%AT4_)V+tGnlaJ;^-V1;eg zXDK)eOmhkQ`ujl!m1CnfxFV%kWuNh121Qpn6skJC8ast2y!`=!V}vx6ucbo70jnR?p~NBhl94hGX*{e`jMZTPnL|il zuZHe_*7U!ziWBN>-v)*bGREd&W{PZfUusDSgP%Q`cA$A025#hL? z;4?#r+REtfyqSRsVz~#?l5o}!|Dt91XrIKxqgnY3*J=kteEPJEscBkMTUK_qoSq&w zxq!0`vva@qf1XF{qc(lQAShxLGhl*m?q#WT41E8NfJm$VLGXzaC)$(82dox-^|p0D zzX6X8doO`*&cKC)D}%QcH$wUMG0_MGx<-O7caR?bEJ>W(UCyr$jEs!*Nd^!Ctq^yi zlC&T4uhX;JKCjuixB}&wsGW|x+|qJ+%a&*X1w9Q6OwGx$`JSl@nPDK5#JQT;>oAC; z+ny-ele35->c@a3V_LSv53^_xy87JG!pzQo7C54Sc4Tea>I5afMLSRR8lCgBNhAsC z3UEzqJlSL8;_%4GTg_}ozz~oXS)PXldmUmM5I%8$m?wu$(dX&eKk{k90HsqW`NEhJ z=%{Gg>x#^JySM-B)?9dAII9MkVn}GnDk|awa&!Q_2m-d|4>96wdXZQD3kQf!;z>dP}_3$(XlF@KL6 z@W+04ZjRkG%GUV*oqEA*MYS4fsw(lf>58*aI>&usx2~X$2qrK$Aze}YN9l@l80Vdi ze&NR1)2QWORM}s#Lgdcz)OH)Ur<;Yg1>glgY`go?3i9j;&~F!3R^*kH&#}ip|2{cLUWLd#JAYba>n2am_xF5>J^i5FUI(RGU9#{J*>3m@ zX=SI7&7kpya5+&+^KaQv7J+1)w|C#}#p)k6=B}x2ZN0PXbl9C^!+YAlIgE@s^m%_Z4g_>5D2IQg{LOzf#LQd{C7VkgPq|6I< z@;tPBP>4e({*b&V-TdU{Wwces%zVY76_aP2s6P#02nQVX$@== z|8U=NWu%<=)TvVtGFOB#4B&LnU%w^*FA4D1{{H^OD_8J=)k6kMNXPDv+WA5ageo9{ z39SXX^^<#9pJ6zlJ=x9g1m!ujjg<|egUwSw-y==0TM zaLw==H)W4tObwN}cizO&k(Y(6_4$^bQ(Ep{LXm;6WL|E8P4NbDEV{XxMm~RzMPlPH zgho!}-~$j76DNwhpSzjv^wH(kL2>=@k@)COdDbW;5+($+K2 zRPd&2IMRnnV#)`3u7;k}6G&J{k&5r;u3>RBQf`9-SvP=+H${F`EB#&8HSmYrn zHC*YO2^lKZgm3Vb0mDkxki9T0pYZfnMEc(Jwb!?`wq7n(V-^z91dQc$IF?=v0dp4|+-B{oBRKvz@&$8>{j6%7tH=5Ssb zJui(}bUjFF5hg)9e|maGx6Zo~ro0Ab_Q5$S@!YZin9a`2EC*(9%#h8Ru#$*r-ACH% z&Z#q1mlwaLZ`I|Uos7Y3Zd^F5cUh0M5KTG|MoNQeyWgiae$b;$Qy3XkgaL4z267;$ zL6yttM;12YVY82fJ6Re^D|s=`KgW|J{AfRX_yE!Vci{al!;BoT(0j77Px7+^5Dj>G zCoiP=(gqz+wMCUWOv&nSP#9D=TnFuKxX3)zX#D6TMqV4o4TlN|!0(3kbs6I> zx0Ld;d??AHeZ-B67jqCFuMUikt}L_d3!*#sPP3oB${~iLU8;MVA4GW+D)VVf2I1#d z1l3pvHn|oq1N;Wgboqo&&xiZjubw}D15%HX7CtkFxVRqcOJ(@7DAviZKdu^K90Yb> z<*_;K*Am!sv}07`Q3xSQqn0ye8xbZyg*5rXr4i>WA!uXlv!}D%a)S;bFKOnyqGhiyt53az*ZF1#a)9$3{vYYeT~L`H)S2#_a^NQr+l; zglu>xV6SGSr8VZFJL7oe0Oc%^dap|tKj0R=d>LYZH_(&*#9k+9m9g&*_*ZmdVh-@n zWpF7`Wt4S_a1*!oZj9(4k=1ZPqhG)N04I*x9kP7!4xEQ1DU@!4knM|Bv!AU`R_swI z5NKA%YxN+U5eT^eL6{8yVAEil5jo?kp*j+qXQb(~0Y#B;GlC-ZO zl+zwLVoB_|fHDosMVz8FCnv|S1 zZ>v1FGO$60*ww}S`V_CFYs!04*lIn;M81gni^6&)>Vc7!V zdC4NVKj}XVQ!Ht@zK5GZ*n!n>dT~wIK!A(jx;t*q$c$cN78ce5W8lR*=$5czMHu)K zzTuWF9;3On3uLVOa~yRdA-8i4o&`ZdU6@5p1{iJF+1A$91F1w77}8L#a+-anxBArH z!QvE7R!)uz9HCQUD-0^@tCSS(tobM6oYJY z#DB9esPi705rXeJz&r2Xy&DDcf*^9&CRe*q7N(ScR^+_>k&i;nG!xy9?j2|4+kdvpw zL*;F5nPJz|GenM&hx?ng$FP8taF!>&e}AJU^-CTCpIyY#hu@b)Ap9)6ESRHD_+}JU*K1rIy9tL_X^+vDR z19L#tjh}}^vDVF-&%pJ{02%Ze8{18A2OySFPlx&i-^Io|`=qYSo5KA&g3ot&^5hA& zyuqZ$>R_Q9d??gYc*Ls@J_oApZVOnH4H61qbz z$H~vH4$+kl`T6UP62D`U?qo@1Z&%k_u-)FjecO;$WKc?3hwUn~M4@G4HJ)1#n9BM` z+>{r#jV(m{{RQ-h2;qt|(!LLSD+IA1PNO<~*zU`Y z-e3w1*LW5~N-sJp>MbafZ8k#IkX5paUHwqIk^4rp%W>?5%WSu&(ZDH(Hq5B|*8zG= zcP-<9wVj@0e-g^`?;^er%~^o)#~FJ}Bd z6gEOUU%{t z-{&6`zb@uCjHlfqR(?vpf62a)K!3~PdRbb&;yIIad#H@B#ohDZ4&2w%%Z3>odI=AQ z@3Hu2*y~J**=-6=A;Q=|hkMoZpbK1e>@|YBM0k+QOUMmf_U0Q@;sUr^s)$IzxlL!Q1 zc)^MP`S%IDClfkjW2?(-2!sNOr6vO5_ZHyZ|NZ`#D-gcp3UHU_HRKwMv`jRX*47w$ zm+|N|sgyd2+@6!f#hKPMNCYH(v)FPaSBNesD7aqVpUuxs`0k*Fn&l+d+~Ai7$tRjW zn_omA&|SYn$kMk9^_R}D6Pisd&=Q_HjtHbYI!i_Cj*EjplymA7$!li2%1^v3m`;ZY z+uPZixmauu%@AckkNC&~gCQ}kW&m7X0A7OnxweZBA8S_yfp?L~!YLQuBbY_Wfq(=9 zavBtYpd6{juChyK+0#re{xNlzj|67a*pyCu@3k_AMBZ zys4!0J&IWo%W{)M98(9jVALWTUSJsv^5=bz`oK27+-g1&_^DvDYIHz`m6i4DgTe(E zRM9QBOJeX^xCEdwJu@@Are?Iu5yFPG5j$E6Dk@>`khq?Z_^CxNWdnBV2D@6oc}}JF zXmJ3XZx%i}BprKv5c2Onc#sMaHkK@{+R8RW zW5dLpHcUssEOeG-3aZ~6iB7PDaO|sh$j})e-5Pi9ya9{>q`T4lVDwskUY;r_bLucn zvvtAl8aunXj!rzwVgo*7FXVJCA00s)fzfxNbil9&n6o)w{Hqr)QbELO8Kn*uH-|(3 zn}me^iwlg5`H&N8F|{c6{8&G8uv`zH&XB89+-xpsH&&GnpZEfa23*UjH4kp?dho$A(-=xG@4UgJHz=9EczAk*JlfpI7m-P=OL!d$9-5eMdj&}kLmRHwhm?busceZP|8oyxA2w*W|D zVJ8vrCZwIW2Zwn8MH+TiV`}AOW%0c2?O~%w!>|^oVU4!$@vlt4W@$jKP{+UKQRkjq zQEpG-tD#rY^1&Rr1ME=`2?=>|eB9hWHkJ#wV05MkVgGBHE`iVd83ZC+Vb#PwZc6f; zs}UK88qrx+p*{{t*#mFM?PPwIfwM1N1qk2$D*(wu*m4o4YMSLYp>)SkHN2aP*0culo|A}=$IHbF)>}BMp%Ui8p(U0hbRES*g^t3 z{<1QR{9&;sv>{X6imk%OU#T8}ru!XcuuAPG>KIzEWDac5?7BKbAai2g`vp+*AXzZ) zHk3EK{5KG?(~#=IkA3M^5B~%mT{i!l(9yI36po$*`#Rue9I?PyO(DUCRSX$Ce~>D7 z18OH{WLzH%@!sDsL1B*einKYnMx1?*R-qGaLkuYkKCY4xwH;M=uqLkD4WCMo3m_X9 z{?4%RvcCNR-WMPoCkQ3@hUel~lEhqZLf||WsvwNlm0&l%VXZEF5W$*zvS|I@crCK8 zyE}DydRl5^UfFXuJfvNHP_(mNrVKtFBK_UF|7RJ%(;u3v_L_gr&O2kT!kZY;n61+! zp#y9PB+fe&sc?Qja8uq{;JG)zZ70CyaA;-aPChvsix=L9oCU*x3yT>T9&ErEwmWaW z;kp@&H~#Tz%_2pq5KSb2FDrhC;pQ>zThy| z>!{Qth+p6##w Yxsz%iUQW!~0hD9#boFyt=akR{0L92vIsgCw literal 0 HcmV?d00001 diff --git a/frame.go b/frame.go index cc2fca9..90bcbf5 100644 --- a/frame.go +++ b/frame.go @@ -10,7 +10,10 @@ import ( type Frame struct { Name string BaseWidget - packs map[Side][]packedWidget + + // Widget placement settings. + packs map[Side][]packedWidget // Packed widgets + placed []placedWidget // Placed widgets widgets []Widget } @@ -48,6 +51,7 @@ func (w *Frame) Children() []Widget { // Compute the size of the Frame. func (w *Frame) Compute(e render.Engine) { w.computePacked(e) + w.computePlaced(e) } // Present the Frame. diff --git a/frame_place.go b/frame_place.go new file mode 100644 index 0000000..2b2c00d --- /dev/null +++ b/frame_place.go @@ -0,0 +1,97 @@ +package ui + +import ( + "git.kirsle.net/go/render" +) + +// Place provides configuration fields for Frame.Place(). +type Place struct { + // X and Y coordinates for explicit location of widget within its parent. + // This placement option trumps all others. + Point render.Point + + // Place relative to an edge of the window. The widget will stick to the + // edge of the window even as it resizes. Options are ignored if Point + // is set. + Top int + Left int + Right int + Bottom int + Center bool + Middle bool +} + +// Strategy returns the placement strategy for a Place config struct. +// Returns 'Point' if a render.Point is used (even if zero, zero) +// Returns 'Side' if the side values are set. +func (p Place) Strategy() string { + if p.Top != 0 || p.Left != 0 || p.Right != 0 || p.Bottom != 0 || p.Center || p.Middle { + return "Side" + } + return "Point" +} + +// placedWidget holds the data for a widget placed in a frame. +type placedWidget struct { + widget Widget + place Place +} + +// Place a widget into the frame. +func (w *Frame) Place(child Widget, config Place) { + w.placed = append(w.placed, placedWidget{ + widget: child, + place: config, + }) + w.widgets = append(w.widgets, child) + + // Adopt the child widget so it can access the Frame. + child.SetParent(w) +} + +// computePlaced processes all the Place layout widgets in the Frame, +// determining their X,Y location and whether they need to change. +func (w *Frame) computePlaced(e render.Engine) { + var ( + frameSize = w.BoxSize() + ) + + for _, row := range w.placed { + // X,Y placement takes priority. + switch row.place.Strategy() { + case "Point": + row.widget.MoveTo(row.place.Point) + case "Side": + var moveTo render.Point + + // Compute the initial X,Y based on Top, Left, Right, Bottom. + if row.place.Left > 0 { + moveTo.X = row.place.Left + } + if row.place.Top > 0 { + moveTo.Y = row.place.Top + } + if row.place.Right > 0 { + moveTo.X = frameSize.W - row.widget.Size().W - row.place.Right + } + if row.place.Bottom > 0 { + moveTo.Y = frameSize.H - row.widget.Size().H - row.place.Bottom + } + + // Center and Middle aligned values override Left/Right, Top/Bottom + // settings respectively. + if row.place.Center { + moveTo.X = frameSize.W - (w.Size().W / 2) - (row.widget.Size().W / 2) + } + if row.place.Middle { + moveTo.Y = frameSize.H - (w.Size().H / 2) - (row.widget.Size().H / 2) + } + row.widget.MoveTo(moveTo) + } + + // If this widget itself has placed widgets, call its function too. + if frame, ok := row.widget.(*Frame); ok { + frame.computePlaced(e) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..630c5e6 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.kirsle.net/go/ui + +go 1.13 + +require ( + git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d + github.com/veandco/go-sdl2 v0.4.1 // indirect + golang.org/x/image v0.0.0-20200119044424-58c23975cae1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..044cc79 --- /dev/null +++ b/go.sum @@ -0,0 +1,7 @@ +git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d h1:vErak6oVRT2dosyQzcwkjXyWQ2NRIVL8q9R8NOUTtsg= +git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d/go.mod h1:ywZtC+zE2SpeObfkw0OvG01pWHQadsVQ4WDKOYzaejc= +github.com/veandco/go-sdl2 v0.4.1 h1:HmSBvVmKWI8LAOeCfTTM8R33rMyPcs6U3o8n325c9Qg= +github.com/veandco/go-sdl2 v0.4.1/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/image.go b/image.go index d58e740..4ad251b 100644 --- a/image.go +++ b/image.go @@ -2,6 +2,9 @@ package ui import ( "fmt" + "image" + "image/jpeg" + "os" "path/filepath" "strings" @@ -13,8 +16,9 @@ type ImageType string // Supported image formats. const ( - BMP ImageType = "bmp" - PNG = "png" + BMP ImageType = "bmp" + PNG = "png" + JPEG = "jpg" ) // Image is a widget that is backed by an image file. @@ -23,6 +27,7 @@ type Image struct { // Configurable fields for the constructor. Type ImageType + Image image.Image texture render.Texturer } @@ -48,6 +53,29 @@ func ImageFromTexture(tex render.Texturer) *Image { } } +// ImageFromFile creates an Image by opening a file from disk. +func ImageFromFile(e render.Engine, filename string) (*Image, error) { + fh, err := os.Open(filename) + if err != nil { + return nil, err + } + + img, err := jpeg.Decode(fh) + if err != nil { + return nil, err + } + + tex, err := e.StoreTexture(filename, img) + if err != nil { + return nil, err + } + + return &Image{ + Image: img, + texture: tex, + }, nil +} + // OpenImage initializes an Image with a given file name. // // The file extension is important and should be a supported ImageType. @@ -58,6 +86,10 @@ func OpenImage(e render.Engine, filename string) (*Image, error) { w.Type = BMP case ".png": w.Type = PNG + case ".jpg": + w.Type = JPEG + case ".jpeg": + w.Type = JPEG default: return nil, fmt.Errorf("OpenImage: %s: not a supported image type", filename) } @@ -71,6 +103,19 @@ func OpenImage(e render.Engine, filename string) (*Image, error) { return w, nil } +// GetRGBA returns an image.RGBA from the image data. +func (w *Image) GetRGBA() *image.RGBA { + var bounds = w.Image.Bounds() + var rgba = image.NewRGBA(bounds) + for x := bounds.Min.X; x < bounds.Max.X; x++ { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + color := w.Image.At(x, y) + rgba.Set(x, y, color) + } + } + return rgba +} + // Compute the widget. func (w *Image) Compute(e render.Engine) { w.Resize(w.texture.Size()) diff --git a/main_window.go b/main_window.go index 3678c9d..d90faa4 100644 --- a/main_window.go +++ b/main_window.go @@ -16,6 +16,12 @@ var ( FPS = 60 ) +// Default width and height for MainWindow. +var ( + DefaultWidth = 640 + DefaultHeight = 480 +) + // MainWindow is the parent window of a UI application. type MainWindow struct { Engine render.Engine @@ -27,11 +33,26 @@ type MainWindow struct { } // NewMainWindow initializes the MainWindow. You should probably only have one -// of these per application. -func NewMainWindow(title string) (*MainWindow, error) { +// of these per application. Dimensions are the width and height of the window. +// +// Example: NewMainWindow("Title Bar") // default 640x480 window +// NewMainWindow("Title", 800, 600) // both required +func NewMainWindow(title string, dimensions ...int) (*MainWindow, error) { + var ( + width = DefaultWidth + height = DefaultHeight + ) + + if len(dimensions) > 0 { + if len(dimensions) != 2 { + return nil, fmt.Errorf("provide width and height dimensions, like NewMainWindow(title, 800, 600)") + } + width, height = dimensions[0], dimensions[1] + } + mw := &MainWindow{ - w: 800, - h: 600, + w: width, + h: height, supervisor: NewSupervisor(), loopCallbacks: []func(*event.State){}, } @@ -56,6 +77,11 @@ func NewMainWindow(title string) (*MainWindow, error) { return mw, nil } +// SetTitle changes the title of the window. +func (mw *MainWindow) SetTitle(title string) { + mw.Engine.SetTitle(title) +} + // Add a child widget to the window. func (mw *MainWindow) Add(w Widget) { mw.supervisor.Add(w) @@ -67,6 +93,12 @@ func (mw *MainWindow) Pack(w Widget, pack Pack) { mw.frame.Pack(w, pack) } +// Place a child widget into the window's default frame. +func (mw *MainWindow) Place(w Widget, config Place) { + mw.Add(w) + mw.frame.Place(w, config) +} + // Frame returns the window's main frame, if needed. func (mw *MainWindow) Frame() *Frame { return mw.frame