UI changes; typing state working

This commit refactors the input container's UI as well as fixing some
bugs related to asynchronous fetching of images.

It also adds complete typing indicator capabilities, all without using a
single mutex!
This commit is contained in:
diamondburned (Forefront) 2020-07-03 21:41:12 -07:00 committed by diamondburned
parent 68814c2207
commit 2fae6ffbb3
19 changed files with 464 additions and 360 deletions

10
go.mod
View File

@ -7,13 +7,11 @@ replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200630
require (
github.com/Xuanwo/go-locale v0.2.0
github.com/alecthomas/chroma v0.7.3
github.com/diamondburned/cchat v0.0.39
github.com/diamondburned/cchat-discord v0.0.0-20200701182710-73a7393e7846
github.com/diamondburned/cchat-mock v0.0.0-20200630025821-605d61d89288
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64
github.com/diamondburned/cchat v0.0.40
github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3
github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516
github.com/goodsign/monday v1.0.0
github.com/google/btree v1.0.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
github.com/ianlancetaylor/cgosymbolizer v0.0.0-20200424224625-be1b05b0b279

63
go.sum
View File

@ -21,11 +21,14 @@ github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00
github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y=
github.com/Xuanwo/go-locale v0.2.0 h1:1N8SGG2VNpLl6VVa8ueZm3Nm+dxvk8ffY9aviKHl4IE=
github.com/Xuanwo/go-locale v0.2.0/go.mod h1:6qbT9M726OJgyiGZro2YwPmx63wQzlH+VvtjJWQoftw=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI=
github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@ -39,48 +42,20 @@ github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb h1:Ja/niwykeFoSkYxdRRzM8QUAuCswfLmaiBTd2UIU+54=
github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb/go.mod h1:q1MbMBfZrv7xqV8n7LgMwhHs3oBbNwWJes8exs2AmDs=
github.com/diamondburned/arikawa v0.9.5 h1:P1ffsp+NHT22wWKYFVC8CdlGRLzPuUV9FcCBKOCJpCI=
github.com/diamondburned/arikawa v0.9.5/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
github.com/diamondburned/cchat v0.0.28 h1:+1VnltW0rl8/NZTUP+x89jVhi3YTTR+e6iLprZ7HcwM=
github.com/diamondburned/cchat v0.0.28/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.31 h1:yUgrh5xbGX0R55glyxYtVewIDL2eXLJ+okIEfVaVoFk=
github.com/diamondburned/cchat v0.0.31/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.32 h1:nLiD4sL9+DLBnvNb9XLidd5peRzTgM9lWcqRsUmm474=
github.com/diamondburned/cchat v0.0.32/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.34 h1:BGiVxMRA9dmW3rLilIldBvjVan7eTTpaWCCfX9IKBYU=
github.com/diamondburned/cchat v0.0.34/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.35 h1:WiMGl8BQJgbP9E4xRxgLGlqUsHpTcJgDKDt8/6a7lBk=
github.com/diamondburned/cchat v0.0.35/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.37 h1:yGz9yls5Lb/vLkU/DU53GjC80WOqoRe229DXsu5mtaY=
github.com/diamondburned/cchat v0.0.37/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.39 h1:Hxd7swmAIECm0MBd5wb1IFvreChwDFwnAshqgAstWGA=
github.com/diamondburned/cchat v0.0.39/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3 h1:8RCcaY3gtA+8NG2mwkcC/PIFK+eS8XnGyeVaUbCXbF0=
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3/go.mod h1:4q0jHEl1gJEzkS92oacwcSf9+3fFcNPukOpURDJpV/A=
github.com/diamondburned/cchat-discord v0.0.0-20200629023132-0a66a65fcf27 h1:MAWs83jAZKxkU04RAoaewc1xUrLKlB6F5X1ViFRkWdw=
github.com/diamondburned/cchat-discord v0.0.0-20200629023132-0a66a65fcf27/go.mod h1:af673uNyL2NSjYqJ54DotPV/iNY+OvBcQfDqHefbap4=
github.com/diamondburned/cchat-discord v0.0.0-20200630031444-53111f3186b3 h1:NDh2osOfPBtiNzz8ro8P7vN4f91uuVMMBJ8+Q2s/EQo=
github.com/diamondburned/cchat-discord v0.0.0-20200630031444-53111f3186b3/go.mod h1:hj6qS/5TOiIxwWyFMts51ILzY9M3GKbXT31hjVbr9gM=
github.com/diamondburned/cchat-discord v0.0.0-20200701182710-73a7393e7846 h1:LLgI89H9A4ZVf8U7bHmOSkG12b8B6a9dwo32Bq7+0AI=
github.com/diamondburned/cchat-discord v0.0.0-20200701182710-73a7393e7846/go.mod h1:lLWRY7l6cC3bI2ge+Vc7pEhgbIL8XYCeXt+h1PWA/1k=
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d h1:LkzARyvdGRvAsaKEPTV3XcqMHENH6J+KRAI+3sq41Qs=
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d/go.mod h1:SVTt5je4G+re8aSVJAFk/x8vvbRzXdpKgSKmVGoM1tg=
github.com/diamondburned/cchat-mock v0.0.0-20200620231423-b286a0301190 h1:mHbA/xhL584IToD38m3QkeU1cRWIPzBZCDFi1Wv0Bz4=
github.com/diamondburned/cchat-mock v0.0.0-20200620231423-b286a0301190/go.mod h1:u4aWUu4be+JkuMtTIGJNS79T1b+8ruijVn9qL/LM4Rk=
github.com/diamondburned/cchat-mock v0.0.0-20200628063912-3155c1b6d6a9 h1:PnVUCrLTsQlafutS13ST7292WkBpiMJzA7125q02LkA=
github.com/diamondburned/cchat-mock v0.0.0-20200628063912-3155c1b6d6a9/go.mod h1:kKtLZzvkJdeLpiNVmwe15Bl202gfbIHC/LwWUbsSnls=
github.com/diamondburned/cchat-mock v0.0.0-20200630025821-605d61d89288 h1:ApNV7DUyj+LG5lUxtTv4yvQZcprsDgZji0iiD+LYJmM=
github.com/diamondburned/cchat-mock v0.0.0-20200630025821-605d61d89288/go.mod h1:Tu+8b1iz9NGeQb2jmndXn+dQ9zBUa8a8ktK9hL5aaxw=
github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6 h1:ZzLrfQqszhzWI7zqwltzQIWtppfcL7m2aIEpB4kuqx0=
github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/cchat v0.0.40 h1:38gPyJnnDoNDHrXcV8Qchfv3y6jlS3Fzz/6FY0BPH6I=
github.com/diamondburned/cchat v0.0.40/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03 h1:F5TL7GPRU/D4ldVkS0haY3SiHPtf1Kby/4nbYpm//MQ=
github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03/go.mod h1:p0X6QUH0mxK8yEW0+a4QA77ClAmoxz8CvgbnobMtWQA=
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3 h1:xr07/2cwINyrMqh92pQQJVDfQqG0u6gHAK+ZcGfpSew=
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3/go.mod h1:SRu3OOeggELFr2Wd3/+SpYV1eNcvSk2LBhM70NOZSG8=
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI=
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64 h1:/ykUYHuYyj+NN/aaqe6lfaCZQc3EMZs93wAGVJTh5j0=
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
github.com/diamondburned/ningen v0.1.0 h1:cTnRNrN0g2Wr/kgjLLpa3pqlbEd6JPNa1yGDer8uV4U=
github.com/diamondburned/ningen v0.1.0/go.mod h1:1vi8mlBlM2xjJy+IugU51q+IMgyNXggS4Xv3SPFd2Q4=
github.com/diamondburned/ningen v0.1.1-0.20200621003851-e5908c53bf21 h1:luSZOjnwoHCbuw5mG3muuDSOUv5YCbFbbjw17dKZ1Ik=
github.com/diamondburned/ningen v0.1.1-0.20200621003851-e5908c53bf21/go.mod h1:1vi8mlBlM2xjJy+IugU51q+IMgyNXggS4Xv3SPFd2Q4=
github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516 h1:6j4oZahbNdVhSEInRfeYbgDpx1FXDfJy6CcUVyWOuVY=
github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I=
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
@ -125,8 +100,6 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
@ -146,15 +119,21 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/markbates/pkger v0.17.0 h1:RFfyBPufP2V6cddUyyEVSHBpaAnM1WzaMNyqomeT+iY=
github.com/markbates/pkger v0.17.0/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@ -166,6 +145,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
@ -262,12 +242,15 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=

View File

@ -1,7 +1,6 @@
package gts
import (
"context"
"os"
"time"
@ -159,93 +158,21 @@ func ExecSync(fn func()) <-chan struct{} {
return ch
}
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
// main loop.
func AfterFunc(d time.Duration, f func()) (stop func()) {
h, err := glib.TimeoutAdd(
uint(d.Milliseconds()),
func() bool { f(); return true },
)
if err != nil {
panic(err)
}
return func() { glib.SourceRemove(h) }
}
func EventIsRightClick(ev *gdk.Event) bool {
keyev := gdk.EventButtonNewFromEvent(ev)
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
}
// Reuser is an interface for structs that inherit Reusable.
type Reuser interface {
Context() context.Context
Acquire() int64
Validate(int64) bool
}
type AsyncUser = func(context.Context) (interface{}, error)
// AsyncUse is a handler for structs that implement the Reuser primitive. The
// passed in function will be called asynchronously, but swap will be called in
// the Gtk main thread.
func AsyncUse(r Reuser, swap func(interface{}), fn AsyncUser) {
// Acquire an ID.
id := r.Acquire()
ctx := r.Context()
Async(func() (func(), error) {
// Run the callback asynchronously.
v, err := fn(ctx)
if err != nil {
return nil, err
}
return func() {
// Validate the ID. Cancel if it's invalid.
if !r.Validate(id) {
log.Println("Async function value dropped for reusable primitive.")
return
}
// Update the resource.
swap(v)
}, nil
})
}
// Reusable is the synchronization primitive to provide a method for
// asynchronous cancellation and reusability.
//
// It works by copying the ID (time) for each asynchronous operation. The
// operation then completes, and the ID is then compared again before being
// used. It provides a cancellation abstraction around the Gtk main thread.
//
// This struct is not thread-safe, as it relies on the Gtk main thread
// synchronization.
type Reusable struct {
time int64 // creation time, used as ID
ctx context.Context
cancel func()
}
func NewReusable() *Reusable {
r := &Reusable{}
r.Invalidate()
return r
}
// Invalidate generates a new ID for the primitive, which would render
// asynchronously updating elements invalid.
func (r *Reusable) Invalidate() {
// Cancel the old context.
if r.cancel != nil {
r.cancel()
}
// Reset.
r.time = time.Now().UnixNano()
r.ctx, r.cancel = context.WithCancel(context.Background())
}
// Context returns the reusable's cancellable context. It never returns nil.
func (r *Reusable) Context() context.Context {
return r.ctx
}
// Reusable checks the acquired ID against the current one.
func (r *Reusable) Validate(acquired int64) (valid bool) {
return r.time == acquired
}
// Acquire lends the ID to be given to Reusable() after finishing.
func (r *Reusable) Acquire() int64 {
return r.time
}

View File

@ -57,7 +57,7 @@ func AsyncImageSized(img ImageContainerSizer, url string, w, h int, procs ...img
}
// Add a processor to resize.
procs = imgutil.Prepend(imgutil.Resize(w, h), procs)
procs = append(procs, imgutil.Resize(w, h))
ctx, cancel := context.WithCancel(context.Background())
connectDestroyer(img, cancel)

View File

@ -59,8 +59,9 @@ type Controller interface {
BindMenu(GridMessage)
// Bottomed returns whether or not the message scroller is at the bottom.
Bottomed() bool
// ScrollToBottom scrolls the message view to the bottom.
// ScrollToBottom()
// AuthorEvent is called on message create/update. This is used to update
// the typer state.
AuthorEvent(a cchat.MessageAuthor)
}
// Constructor is an interface for making custom message implementations which

View File

@ -26,7 +26,6 @@ func NewGridStore(constr Constructor, ctrl Controller) *GridStore {
grid.SetRowSpacing(5)
grid.SetMarginStart(5)
grid.SetMarginEnd(5)
grid.SetMarginBottom(5)
grid.Show()
primitives.AddClass(grid, "message-grid")
@ -230,6 +229,9 @@ func (c *GridStore) AddPresendMessage(msg input.PresendMessage) PresendGridMessa
}
func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())
// Attempt to update before insertion (aka upsert).
if msgc := c.Message(msg); msgc != nil {
msgc.UpdateAuthor(msg.Author())
@ -253,6 +255,9 @@ func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
}
func (c *GridStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())
if msgc := c.Message(msg); msgc != nil {
if author := msg.Author(); author != nil {
msgc.UpdateAuthor(author)

View File

@ -23,19 +23,31 @@ type InputView struct {
}
var textCSS = primitives.PrepareCSS(`
textview, textview * {
.message-input, .message-input * {
background-color: transparent;
}
.message-input * {
background-color: @theme_base_color;
border: 1px solid alpha(@theme_fg_color, 0.2);
border-radius: 4px;
transition: linear 50ms border-color;
}
.message-input:focus * {
border-color: @theme_selected_bg_color;
}
`)
func NewView(ctrl Controller) *InputView {
text, _ := gtk.TextViewNew()
text.SetSensitive(false)
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
text.SetProperty("top-margin", inputmargin)
text.SetProperty("left-margin", inputmargin)
text.SetProperty("right-margin", inputmargin)
text.SetProperty("bottom-margin", inputmargin)
text.SetVAlign(gtk.ALIGN_START)
text.SetProperty("top-margin", 4)
text.SetProperty("bottom-margin", 4)
text.SetProperty("left-margin", 8)
text.SetProperty("right-margin", 8)
text.Show()
primitives.AddClass(text, "message-input")
@ -79,11 +91,14 @@ type Field struct {
editingID string // never empty
}
const inputmargin = username.VMargin
var scrollinputCSS = primitives.PrepareCSS(`
.scrolled-input {
margin: 5px;
}
`)
func NewField(text *gtk.TextView, ctrl Controller) *Field {
username := username.NewContainer()
username.SetVAlign(gtk.ALIGN_END)
username.Show()
buf, _ := text.GetBuffer()
@ -91,6 +106,9 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
sw := scrollinput.NewV(text, 150)
sw.Show()
primitives.AddClass(sw, "scrolled-input")
primitives.AttachCSS(sw, scrollinputCSS)
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(username, false, false, 0)
box.PackStart(sw, true, true, 0)

View File

@ -1,141 +0,0 @@
package input
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/config"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
)
const AvatarSize = 24
var showUser = true
var currentRevealer = func(bool) {} // noop by default
func init() {
// Bind this revealer in settings.
config.AppearanceAdd("Show Username in Input", config.Switch(
&showUser,
func(b bool) { currentRevealer(b) },
))
}
type usernameContainer struct {
*gtk.Revealer
main *gtk.Box
avatar *rich.Icon
label *rich.Label
}
var (
_ cchat.LabelContainer = (*usernameContainer)(nil)
_ cchat.IconContainer = (*usernameContainer)(nil)
)
func newUsernameContainer() *usernameContainer {
avatar := rich.NewIcon(AvatarSize, imgutil.Round(true))
avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
avatar.Show()
label := rich.NewLabel(text.Rich{})
label.SetMaxWidthChars(35)
label.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
box.PackStart(avatar, false, false, 0)
box.PackStart(label, false, false, 0)
box.SetMarginStart(10)
box.SetMarginEnd(10)
box.SetMarginTop(inputmargin)
box.SetMarginBottom(inputmargin)
box.SetVAlign(gtk.ALIGN_START)
box.Show()
rev, _ := gtk.RevealerNew()
rev.SetRevealChild(false)
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
rev.SetTransitionDuration(50)
rev.Add(box)
// Bind the current global revealer to this revealer for settings. This
// operation should be thread-safe, as everything is being done in the main
// thread.
currentRevealer = rev.SetRevealChild
return &usernameContainer{
Revealer: rev,
main: box,
avatar: avatar,
label: label,
}
}
func (u *usernameContainer) SetRevealChild(reveal bool) {
// Only reveal if showUser is true.
u.Revealer.SetRevealChild(reveal && showUser)
}
// shouldReveal returns whether or not the container should reveal.
func (u *usernameContainer) shouldReveal() bool {
return !u.label.GetLabel().Empty() && showUser
}
func (u *usernameContainer) Reset() {
u.SetRevealChild(false)
u.avatar.Reset()
u.label.Reset()
}
// Update is not thread-safe.
func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMessageSender) {
// Set the fallback username.
u.label.SetLabelUnsafe(session.Name())
// Reveal the name if it's not empty.
u.SetRevealChild(u.shouldReveal())
// Does sender (aka Server) implement ServerNickname? If yes, use it.
if nicknamer, ok := sender.(cchat.ServerNickname); ok {
u.label.AsyncSetLabel(nicknamer.Nickname, "Error fetching server nickname")
}
// Does session implement an icon? Update if yes.
if iconer, ok := session.(cchat.Icon); ok {
u.avatar.AsyncSetIconer(iconer, "Error fetching session icon URL")
}
}
// GetLabel is not thread-safe.
func (u *usernameContainer) GetLabel() text.Rich {
return u.label.GetLabel()
}
// SetLabel is thread-safe.
func (u *usernameContainer) SetLabel(content text.Rich) {
gts.ExecAsync(func() {
u.label.SetLabelUnsafe(content)
// Reveal if the name is not empty.
u.SetRevealChild(u.shouldReveal())
})
}
// SetIcon is thread-safe.
func (u *usernameContainer) SetIcon(url string) {
gts.ExecAsync(func() {
u.avatar.SetIconUnsafe(url)
// Reveal if the icon URL is not empty. We don't touch anything if the
// URL is empty, as the name might not be.
if url != "" {
u.SetRevealChild(true)
}
})
}
// GetIconURL is not thread-safe.
func (u *usernameContainer) GetIconURL() string {
return u.avatar.URL()
}

View File

@ -4,6 +4,7 @@ import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/config"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
@ -11,7 +12,6 @@ import (
)
const AvatarSize = 24
const VMargin = 4
var showUser = true
var currentRevealer = func(bool) {} // noop by default
@ -36,6 +36,12 @@ var (
_ cchat.IconContainer = (*Container)(nil)
)
var usernameCSS = primitives.PrepareCSS(`
.username-view {
margin: 8px 10px;
}
`)
func NewContainer() *Container {
avatar := rich.NewIcon(AvatarSize, imgutil.Round(true))
avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
@ -48,13 +54,12 @@ func NewContainer() *Container {
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
box.PackStart(avatar, false, false, 0)
box.PackStart(label, false, false, 0)
box.SetMarginStart(10)
box.SetMarginEnd(10)
box.SetMarginTop(VMargin)
box.SetMarginBottom(VMargin)
box.SetVAlign(gtk.ALIGN_START)
box.Show()
primitives.AddClass(box, "username-view")
primitives.AttachCSS(box, usernameCSS)
rev, _ := gtk.RevealerNew()
rev.SetRevealChild(false)
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)

View File

@ -0,0 +1,145 @@
package typing
import (
"sort"
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/pkg/errors"
)
type State struct {
// states
typers []cchat.Typer
timeout time.Duration
canceler func()
invalidated bool
// consts
changed func(s *State, empty bool)
stopper func() // stops the event loop, not used atm
}
var _ cchat.TypingIndicator = (*State)(nil)
func NewState(changed func(s *State, empty bool)) *State {
s := &State{changed: changed}
s.stopper = gts.AfterFunc(time.Second/2, s.loop)
return s
}
func (s *State) reset() {
if s.canceler != nil {
s.canceler()
s.canceler = nil
}
s.timeout = 0
s.typers = nil
s.invalidated = false
}
// Subscribe is thread-safe.
func (s *State) Subscribe(indicator cchat.ServerMessageTypingIndicator) {
gts.Async(func() (func(), error) {
c, err := indicator.TypingSubscribe(s)
if err != nil {
return nil, errors.Wrap(err, "Failed to subscribe to typing indicator")
}
return func() {
s.canceler = c
s.timeout = indicator.TypingTimeout()
}, nil
})
}
// loop runs a single iteration of the event loop. This function is not
// thread-safe.
func (s *State) loop() {
// Filter out any expired typers.
t, ok := filterTypers(s.typers, s.timeout)
if ok {
s.invalidated = true
s.typers = t
}
// Call the event handler if things are invalidated.
if s.invalidated {
s.changed(s, len(s.typers) == 0)
s.invalidated = false
}
}
// invalidate sorts and invalidates the state.
func (s *State) invalidate() {
// Sort the list of typers again.
sort.Slice(s.typers, func(i, j int) bool {
return s.typers[i].Time().Before(s.typers[j].Time())
})
s.invalidated = true
}
// AddTyper is thread-safe.
func (s *State) AddTyper(typer cchat.Typer) {
gts.ExecAsync(func() {
defer s.invalidate()
// If the typer already exists, then pop them to the start of the list.
for i, t := range s.typers {
if t.ID() == typer.ID() {
s.typers[i] = t
return
}
}
s.typers = append(s.typers, typer)
})
}
// RemoveTyper is thread-safe.
func (s *State) RemoveTyper(typerID string) {
gts.ExecAsync(func() { s.removeTyper(typerID) })
}
func (s *State) removeTyper(typerID string) {
defer s.invalidate()
for i, t := range s.typers {
if t.ID() == typerID {
// Remove the quick way. Sort will take care of ordering.
l := len(s.typers) - 1
s.typers[i] = s.typers[l]
s.typers[l] = nil
s.typers = s.typers[:l]
return
}
}
}
func filterTypers(typers []cchat.Typer, timeout time.Duration) ([]cchat.Typer, bool) {
// Fast path.
if len(typers) == 0 || timeout == 0 {
return nil, false
}
var now = time.Now()
var cut int
for _, t := range typers {
if now.Sub(t.Time()) < timeout {
typers[cut] = t
cut++
}
}
for i := cut; i < len(typers); i++ {
typers[i] = nil
}
var changed = cut != len(typers)
return typers[:cut], changed
}

View File

@ -1,29 +1,19 @@
package typing
import (
"strings"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
)
type State struct {
typers []cchat.Typer
}
func NewState() *State {
return &State{}
}
func (s *State) Empty() bool {
// return len(s.typers) == 0
return false
}
var typingIndicatorCSS = primitives.PrepareCSS(`
.typing-indicator {
border-radius: 8px 8px 0 0;
margin: 0 6px;
border-radius: 6px 6px 0 0;
color: alpha(@theme_fg_color, 0.8);
background-color: @theme_base_color;
}
@ -35,42 +25,94 @@ var smallfonts = primitives.PrepareCSS(`
type Container struct {
*gtk.Revealer
empty bool // && state.Empty()
State *State
state *State
}
const placeholder = "Bruh moment..."
func New() *Container {
d := NewDots()
d.Show()
l, _ := gtk.LabelNew(placeholder)
l, _ := gtk.LabelNew("")
l.SetXAlign(0)
l.SetEllipsize(pango.ELLIPSIZE_END)
l.Show()
primitives.AttachCSS(l, smallfonts)
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
b.PackStart(d, false, false, username.VMargin)
b.PackStart(d, false, false, 4)
b.PackStart(l, true, true, 0)
b.SetMarginStart(username.VMargin * 2)
b.SetMarginEnd(username.VMargin * 2)
b.Show()
r, _ := gtk.RevealerNew()
r.SetTransitionDuration(50)
r.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_CROSSFADE)
r.SetRevealChild(true)
r.SetRevealChild(false)
r.Add(b)
state := NewState()
primitives.AddClass(b, "typing-indicator")
primitives.AttachCSS(b, typingIndicatorCSS)
state := NewState(func(s *State, empty bool) {
r.SetRevealChild(!empty)
l.SetMarkup(render(s.typers))
})
// On label destroy, stop the state loop as well.
l.Connect("destroy", state.stopper)
return &Container{
Revealer: r,
State: state,
state: state,
}
}
func (c *Container) Reset() {
c.state.reset()
c.SetRevealChild(false)
}
func (c *Container) RemoveAuthor(author cchat.MessageAuthor) {
c.state.removeTyper(author.ID())
}
func (c *Container) TrySubscribe(svmsg cchat.ServerMessage) bool {
ti, ok := svmsg.(cchat.ServerMessageTypingIndicator)
if !ok {
return false
}
c.state.Subscribe(ti)
return true
}
func render(typers []cchat.Typer) string {
// fast path
if len(typers) == 0 {
return ""
}
var builder strings.Builder
for i, typer := range typers {
builder.WriteString("<b>")
builder.WriteString(parser.RenderMarkup(typer.Name()))
builder.WriteString("</b>")
switch i {
case len(typers) - 2:
builder.WriteString(" and ")
case len(typers) - 1:
// Write nothing if this is the last item.
default:
builder.WriteString(", ")
}
}
if len(typers) == 1 {
builder.WriteString(" is typing.")
} else {
builder.WriteString(" are typing.")
}
return builder.String()
}

View File

@ -14,6 +14,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/sadface"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/typing"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
"github.com/gotk3/gotk3/gtk"
@ -81,6 +82,8 @@ func NewView() *View {
view.Box.PackStart(view.InputView, false, false, 0)
view.Box.Show()
primitives.AddClass(view.Box, "message-view")
// placeholder logo
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256())
logo.Show()
@ -111,6 +114,7 @@ func (v *View) Bottomed() bool { return v.Scroller.Bottomed }
func (v *View) Reset() {
v.state.Reset() // Reset the state variables.
v.Typing.Reset() // Reset the typing state.
v.FaceView.Reset() // Switch back to the main screen.
v.InputView.Reset() // Reset the input.
v.Container.Reset() // Clean all messages.
@ -164,6 +168,8 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func
// Set the cancel handler.
v.state.setcurrent(s)
// Try setting the typing indicator if available.
v.Typing.TrySubscribe(server)
}, nil
})
}
@ -185,6 +191,11 @@ func (v *View) AddPresendMessage(msg input.PresendMessage) func(error) {
}
}
// AuthorEvent should be called on message create/update/delete.
func (v *View) AuthorEvent(author cchat.MessageAuthor) {
v.Typing.RemoveAuthor(author)
}
// LatestMessageFrom returns the last message ID with that author.
func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) {
return v.Container.LatestMessageFrom(userID)

108
internal/ui/rich/async.go Normal file
View File

@ -0,0 +1,108 @@
package rich
import (
"context"
"log"
"reflect"
"time"
"github.com/diamondburned/cchat-gtk/internal/gts"
)
// Reuser is an interface for structs that inherit Reusable.
type Reuser interface {
Context() context.Context
Acquire() int64
Validate(int64) bool
SwapResource(v interface{}, cancel func())
}
type AsyncUser = func(context.Context) (interface{}, func(), error)
// AsyncUse is a handler for structs that implement the Reuser primitive. The
// passed in function will be called asynchronously, but swap will be called in
// the Gtk main thread.
func AsyncUse(r Reuser, fn AsyncUser) {
// Acquire an ID.
id := r.Acquire()
ctx := r.Context()
gts.Async(func() (func(), error) {
// Run the callback asynchronously.
v, cancel, err := fn(ctx)
if err != nil {
return nil, err
}
return func() {
// Validate the ID. Cancel if it's invalid.
if !r.Validate(id) {
log.Println("Async function value dropped for reusable primitive.")
return
}
// Update the resource.
r.SwapResource(v, cancel)
}, nil
})
}
// Reusable is the synchronization primitive to provide a method for
// asynchronous cancellation and reusability.
//
// It works by copying the ID (time) for each asynchronous operation. The
// operation then completes, and the ID is then compared again before being
// used. It provides a cancellation abstraction around the Gtk main thread.
//
// This struct is not thread-safe, as it relies on the Gtk main thread
// synchronization.
type Reusable struct {
time int64 // creation time, used as ID
ctx context.Context
cancel func()
swapfn reflect.Value // reflect fn
arg1type reflect.Type
}
var _ Reuser = (*Reusable)(nil)
func NewReusable(swapperFn interface{}) *Reusable {
r := Reusable{}
r.swapfn = reflect.ValueOf(swapperFn)
r.arg1type = r.swapfn.Type().In(0)
r.Invalidate()
return &r
}
// Invalidate generates a new ID for the primitive, which would render
// asynchronously updating elements invalid.
func (r *Reusable) Invalidate() {
// Cancel the old context.
if r.cancel != nil {
r.cancel()
}
// Reset.
r.time = time.Now().UnixNano()
r.ctx, r.cancel = context.WithCancel(context.Background())
}
// Context returns the reusable's cancellable context. It never returns nil.
func (r *Reusable) Context() context.Context {
return r.ctx
}
// Reusable checks the acquired ID against the current one.
func (r *Reusable) Validate(acquired int64) (valid bool) {
return r.time == acquired
}
// Acquire lends the ID to be given to Reusable() after finishing.
func (r *Reusable) Acquire() int64 {
return r.time
}
func (r *Reusable) SwapResource(v interface{}, cancel func()) {
r.swapfn.Call([]reflect.Value{reflect.ValueOf(v)})
}

View File

@ -16,12 +16,11 @@ type IconerFn = func(context.Context, cchat.IconContainer) (func(), error)
type Icon struct {
*gtk.Revealer
Image *gtk.Image
resizer imgutil.Processor
procs []imgutil.Processor
size int
Image *gtk.Image
procs []imgutil.Processor
size int
r gts.Reusable
r *Reusable
// state
url string
@ -50,10 +49,11 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
Revealer: rev,
Image: img,
procs: procs,
r: *gts.NewReusable(),
}
i.SetSize(sizepx)
i.r = NewReusable(func(ni *nullIcon) {
i.SetIconUnsafe(ni.url)
})
return i
}
@ -61,6 +61,7 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
// Reset wipes the state to be just after construction.
func (i *Icon) Reset() {
i.url = ""
i.r.Invalidate() // invalidate async fetching images
i.Revealer.SetRevealChild(false)
i.Image.SetFromPixbuf(nil) // destroy old pb
}
@ -99,7 +100,6 @@ func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
func (i *Icon) SetSize(szpx int) {
i.size = szpx
i.Image.SetSizeRequest(szpx, szpx)
i.resizer = imgutil.Resize(szpx, szpx)
}
// AddProcessors is not thread-safe.
@ -112,16 +112,11 @@ func (i *Icon) SetIcon(url string) {
gts.ExecAsync(func() { i.SetIconUnsafe(url) })
}
func (i *Icon) swapResource(v interface{}) {
i.SetIconUnsafe(v.(*nullIcon).url)
}
func (i *Icon) AsyncSetIconer(iconer cchat.Icon, wrap string) {
gts.AsyncUse(&i.r, i.swapResource, func(ctx context.Context) (interface{}, error) {
AsyncUse(i.r, func(ctx context.Context) (interface{}, func(), error) {
ni := &nullIcon{}
f, err := iconer.Icon(ctx, ni)
ni.cancel = f
return ni, err
return ni, f, err
})
}
@ -133,7 +128,11 @@ func (i *Icon) SetIconUnsafe(url string) {
}
func (i *Icon) updateAsync() {
httputil.AsyncImage(i.Image, i.url, imgutil.Prepend(i.resizer, i.procs)...)
if i.size > 0 {
httputil.AsyncImageSized(i.Image, i.url, i.size, i.size, i.procs...)
} else {
httputil.AsyncImage(i.Image, i.url, i.procs...)
}
}
type ToggleButtonImage struct {

View File

@ -30,7 +30,7 @@ type Label struct {
current text.Rich
// Reusable primitive.
r gts.Reusable
r *Reusable
}
var (
@ -44,12 +44,17 @@ func NewLabel(content text.Rich) *Label {
label.SetXAlign(0) // left align
label.SetEllipsize(pango.ELLIPSIZE_END)
return &Label{
l := &Label{
Label: *label,
current: content,
// reusable primitive, take reference
r: *gts.NewReusable(),
}
// reusable primitive
l.r = NewReusable(func(nl *nullLabel) {
l.SetLabelUnsafe(nl.Rich)
})
return l
}
// Reset wipes the state to be just after construction.
@ -59,17 +64,11 @@ func (l *Label) Reset() {
l.Label.SetText("")
}
// swapResource is reserved for internal use only.
func (l *Label) swapResource(v interface{}) {
l.SetLabelUnsafe(v.(*nullLabel).Rich)
}
func (l *Label) AsyncSetLabel(fn LabelerFn, info string) {
gts.AsyncUse(&l.r, l.swapResource, func(ctx context.Context) (interface{}, error) {
AsyncUse(l.r, func(ctx context.Context) (interface{}, func(), error) {
nl := &nullLabel{}
f, err := fn(ctx, nl)
nl.cancel = f
return nl, err
return nl, f, err
})
}

View File

@ -17,15 +17,13 @@ func MakeRed(content text.Rich) string {
// used for grabbing text without changing state
type nullLabel struct {
text.Rich
cancel func()
}
func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t }
// used for grabbing url without changing state
type nullIcon struct {
url string
cancel func()
url string
}
func (i *nullIcon) SetIcon(url string) { i.url = url }

View File

@ -21,11 +21,18 @@ type View struct {
Services []*Container
}
var servicesCSS = primitives.PrepareCSS(`
.services {
background-color: @theme_base_color;
}
`)
func NewView() *View {
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.Show()
primitives.AddClass(box, "services")
primitives.AttachCSS(box, servicesCSS)
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)

View File

@ -24,6 +24,10 @@ var monospace = primitives.PrepareCSS(`
}
`)
var commandPadding = primitives.PrepareCSS(`
* { padding: 8px 12px; }
`)
type Session struct {
*gtk.Box
@ -101,6 +105,8 @@ func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
primitives.AddClass(b, "commander")
primitives.AddClass(view, "command-buffer")
primitives.AddClass(input, "command-input")
primitives.AttachCSS(view, commandPadding)
primitives.AttachCSS(input, commandPadding)
return session
}

View File

@ -1,17 +1,10 @@
/* Make CSS more consistent across themes */
headerbar { padding: 0; }
/* Consistent design */
.services button { border-radius: 0; }
.message-content, .message-content text {
background: none;
box-shadow: none;
border: none;
}
popover > box { margin: 6px; }
popover > box {
margin: 6px;
}
.command-buffer, .command-input {
padding: 8px 12px;
}
/* Hack to fix the input bar being high in Adwaita */
.input-field * { min-height: 0; }