mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-01-27 12:26:39 +00:00
Merge pull request #1 from diamondburned/wip
Minor refactors, UI changes, and added full typing indicator
This commit is contained in:
commit
3cb3ad9852
10
go.mod
10
go.mod
|
@ -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
63
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -3,7 +3,6 @@ package container
|
|||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
|
||||
|
@ -47,7 +46,6 @@ type Container interface {
|
|||
DeleteMessageUnsafe(cchat.MessageDelete)
|
||||
|
||||
Reset()
|
||||
ScrollToBottom()
|
||||
|
||||
// AddPresendMessage adds and displays an unsent message.
|
||||
AddPresendMessage(msg input.PresendMessage) PresendGridMessage
|
||||
|
@ -59,6 +57,11 @@ type Container interface {
|
|||
type Controller interface {
|
||||
// BindMenu expects the controller to add actioner into the message.
|
||||
BindMenu(GridMessage)
|
||||
// Bottomed returns whether or not the message scroller is at the bottom.
|
||||
Bottomed() bool
|
||||
// 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
|
||||
|
@ -73,8 +76,8 @@ const ColumnSpacing = 10
|
|||
// GridContainer is an implementation of Container, which allows flexible
|
||||
// message grids.
|
||||
type GridContainer struct {
|
||||
*autoscroll.ScrolledWindow
|
||||
*GridStore
|
||||
Controller
|
||||
}
|
||||
|
||||
// gridMessage w/ required internals
|
||||
|
@ -86,16 +89,9 @@ type gridMessage struct {
|
|||
var _ Container = (*GridContainer)(nil)
|
||||
|
||||
func NewGridContainer(constr Constructor, ctrl Controller) *GridContainer {
|
||||
store := NewGridStore(constr, ctrl)
|
||||
|
||||
sw := autoscroll.NewScrolledWindow()
|
||||
sw.Add(store.Grid)
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
|
||||
sw.Show()
|
||||
|
||||
return &GridContainer{
|
||||
ScrolledWindow: sw,
|
||||
GridStore: store,
|
||||
GridStore: NewGridStore(constr, ctrl),
|
||||
Controller: ctrl,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +102,7 @@ func (c *GridContainer) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
|||
c.GridStore.CreateMessageUnsafe(msg)
|
||||
|
||||
// Determine if the user is scrolled to the bottom for cleaning up.
|
||||
if !c.ScrolledWindow.Bottomed {
|
||||
if !c.Bottomed() {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -136,9 +132,3 @@ func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
|
|||
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
|
||||
gts.ExecAsync(func() { c.DeleteMessageUnsafe(msg) })
|
||||
}
|
||||
|
||||
// Reset is not thread-safe.
|
||||
func (c *GridContainer) Reset() {
|
||||
c.GridStore.Reset()
|
||||
c.ScrolledWindow.Bottomed = true
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
|||
|
||||
// Did the handler wipe old messages? It will only do so if the user is
|
||||
// scrolled to the bottom.
|
||||
if !c.ScrolledWindow.Bottomed {
|
||||
if !c.Bottomed() {
|
||||
// If we're not at the bottom, then we exit.
|
||||
return
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
type GridStore struct {
|
||||
Grid *gtk.Grid
|
||||
*gtk.Grid
|
||||
|
||||
Construct Constructor
|
||||
Controller Controller
|
||||
|
@ -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)
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/completion"
|
||||
"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/primitives/scrollinput"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -20,16 +22,37 @@ type InputView struct {
|
|||
Completer *completion.View
|
||||
}
|
||||
|
||||
var textCSS = primitives.PrepareCSS(`
|
||||
.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")
|
||||
primitives.AttachCSS(text, textCSS)
|
||||
|
||||
// Bind the text event handler to text first.
|
||||
c := completion.New(text)
|
||||
|
||||
|
@ -37,12 +60,7 @@ func NewView(ctrl Controller) *InputView {
|
|||
f := NewField(text, ctrl)
|
||||
f.Show()
|
||||
|
||||
// // Connect to the field's revealer. On resize, we want the autocompleter to
|
||||
// // have the right padding too.
|
||||
// f.username.Connect("size-allocate", func(w gtk.IWidget) {
|
||||
// // Set the autocompleter's left margin to be the same.
|
||||
// c.SetMarginStart(w.ToWidget().GetAllocatedWidth())
|
||||
// })
|
||||
primitives.AddClass(f, "input-field")
|
||||
|
||||
return &InputView{f, c}
|
||||
}
|
||||
|
@ -57,7 +75,7 @@ func (v *InputView) SetSender(session cchat.Session, sender cchat.ServerMessageS
|
|||
|
||||
type Field struct {
|
||||
*gtk.Box
|
||||
username *usernameContainer
|
||||
Username *username.Container
|
||||
|
||||
TextScroll *gtk.ScrolledWindow
|
||||
text *gtk.TextView
|
||||
|
@ -73,10 +91,14 @@ type Field struct {
|
|||
editingID string // never empty
|
||||
}
|
||||
|
||||
const inputmargin = 4
|
||||
var scrollinputCSS = primitives.PrepareCSS(`
|
||||
.scrolled-input {
|
||||
margin: 5px;
|
||||
}
|
||||
`)
|
||||
|
||||
func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||
username := newUsernameContainer()
|
||||
username := username.NewContainer()
|
||||
username.Show()
|
||||
|
||||
buf, _ := text.GetBuffer()
|
||||
|
@ -84,14 +106,18 @@ 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)
|
||||
box.Show()
|
||||
|
||||
field := &Field{
|
||||
Box: box,
|
||||
username: username,
|
||||
Box: box,
|
||||
Username: username,
|
||||
// typing: typing,
|
||||
TextScroll: sw,
|
||||
text: text,
|
||||
buffer: buf,
|
||||
|
@ -102,6 +128,13 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
|||
text.SetFocusVAdjustment(sw.GetVAdjustment())
|
||||
text.Connect("key-press-event", field.keyDown)
|
||||
|
||||
// // Connect to the field's revealer. On resize, we want the autocompleter to
|
||||
// // have the right padding too.
|
||||
// f.username.Connect("size-allocate", func(w gtk.IWidget) {
|
||||
// // Set the autocompleter's left margin to be the same.
|
||||
// c.SetMarginStart(w.ToWidget().GetAllocatedWidth())
|
||||
// })
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
|
@ -113,7 +146,7 @@ func (f *Field) Reset() {
|
|||
f.UserID = ""
|
||||
f.Sender = nil
|
||||
f.editor = nil
|
||||
f.username.Reset()
|
||||
f.Username.Reset()
|
||||
|
||||
// reset the input
|
||||
f.buffer.Delete(f.buffer.GetBounds())
|
||||
|
@ -123,7 +156,7 @@ func (f *Field) Reset() {
|
|||
// disabled. Reset() should be called first.
|
||||
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
||||
// Update the left username container in the input.
|
||||
f.username.Update(session, sender)
|
||||
f.Username.Update(session, sender)
|
||||
f.UserID = session.ID()
|
||||
|
||||
// Set the sender.
|
||||
|
|
|
@ -58,9 +58,9 @@ func (f *Field) sendInput() {
|
|||
f.SendMessage(SendMessageData{
|
||||
time: time.Now().UTC(),
|
||||
content: text,
|
||||
author: f.username.GetLabel(),
|
||||
author: f.Username.GetLabel(),
|
||||
authorID: f.UserID,
|
||||
authorURL: f.username.GetIconURL(),
|
||||
authorURL: f.Username.GetIconURL(),
|
||||
nonce: f.generateNonce(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package input
|
||||
package username
|
||||
|
||||
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"
|
||||
|
@ -23,7 +24,7 @@ func init() {
|
|||
))
|
||||
}
|
||||
|
||||
type usernameContainer struct {
|
||||
type Container struct {
|
||||
*gtk.Revealer
|
||||
main *gtk.Box
|
||||
avatar *rich.Icon
|
||||
|
@ -31,11 +32,17 @@ type usernameContainer struct {
|
|||
}
|
||||
|
||||
var (
|
||||
_ cchat.LabelContainer = (*usernameContainer)(nil)
|
||||
_ cchat.IconContainer = (*usernameContainer)(nil)
|
||||
_ cchat.LabelContainer = (*Container)(nil)
|
||||
_ cchat.IconContainer = (*Container)(nil)
|
||||
)
|
||||
|
||||
func newUsernameContainer() *usernameContainer {
|
||||
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)
|
||||
avatar.Show()
|
||||
|
@ -47,13 +54,12 @@ func newUsernameContainer() *usernameContainer {
|
|||
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()
|
||||
|
||||
primitives.AddClass(box, "username-view")
|
||||
primitives.AttachCSS(box, usernameCSS)
|
||||
|
||||
rev, _ := gtk.RevealerNew()
|
||||
rev.SetRevealChild(false)
|
||||
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
||||
|
@ -65,7 +71,7 @@ func newUsernameContainer() *usernameContainer {
|
|||
// thread.
|
||||
currentRevealer = rev.SetRevealChild
|
||||
|
||||
return &usernameContainer{
|
||||
return &Container{
|
||||
Revealer: rev,
|
||||
main: box,
|
||||
avatar: avatar,
|
||||
|
@ -73,24 +79,24 @@ func newUsernameContainer() *usernameContainer {
|
|||
}
|
||||
}
|
||||
|
||||
func (u *usernameContainer) SetRevealChild(reveal bool) {
|
||||
func (u *Container) 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 {
|
||||
func (u *Container) shouldReveal() bool {
|
||||
return !u.label.GetLabel().Empty() && showUser
|
||||
}
|
||||
|
||||
func (u *usernameContainer) Reset() {
|
||||
func (u *Container) 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) {
|
||||
func (u *Container) Update(session cchat.Session, sender cchat.ServerMessageSender) {
|
||||
// Set the fallback username.
|
||||
u.label.SetLabelUnsafe(session.Name())
|
||||
// Reveal the name if it's not empty.
|
||||
|
@ -108,12 +114,12 @@ func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMes
|
|||
}
|
||||
|
||||
// GetLabel is not thread-safe.
|
||||
func (u *usernameContainer) GetLabel() text.Rich {
|
||||
func (u *Container) GetLabel() text.Rich {
|
||||
return u.label.GetLabel()
|
||||
}
|
||||
|
||||
// SetLabel is thread-safe.
|
||||
func (u *usernameContainer) SetLabel(content text.Rich) {
|
||||
func (u *Container) SetLabel(content text.Rich) {
|
||||
gts.ExecAsync(func() {
|
||||
u.label.SetLabelUnsafe(content)
|
||||
|
||||
|
@ -123,7 +129,7 @@ func (u *usernameContainer) SetLabel(content text.Rich) {
|
|||
}
|
||||
|
||||
// SetIcon is thread-safe.
|
||||
func (u *usernameContainer) SetIcon(url string) {
|
||||
func (u *Container) SetIcon(url string) {
|
||||
gts.ExecAsync(func() {
|
||||
u.avatar.SetIconUnsafe(url)
|
||||
|
||||
|
@ -136,6 +142,6 @@ func (u *usernameContainer) SetIcon(url string) {
|
|||
}
|
||||
|
||||
// GetIconURL is not thread-safe.
|
||||
func (u *usernameContainer) GetIconURL() string {
|
||||
func (u *Container) GetIconURL() string {
|
||||
return u.avatar.URL()
|
||||
}
|
|
@ -81,7 +81,7 @@ func NewEmptyContainer() *GenericContainer {
|
|||
ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
||||
ts.SetXAlign(1) // right align
|
||||
ts.SetVAlign(gtk.ALIGN_END)
|
||||
ts.SetSelectable(true)
|
||||
// ts.SetSelectable(true)
|
||||
ts.Show()
|
||||
|
||||
user, _ := gtk.LabelNew("")
|
||||
|
@ -90,7 +90,7 @@ func NewEmptyContainer() *GenericContainer {
|
|||
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||
user.SetXAlign(1) // right align
|
||||
user.SetVAlign(gtk.ALIGN_START)
|
||||
user.SetSelectable(true)
|
||||
// user.SetSelectable(true)
|
||||
user.Show()
|
||||
|
||||
content, _ := gtk.LabelNew("")
|
||||
|
|
56
internal/ui/messages/typing/dots.go
Normal file
56
internal/ui/messages/typing/dots.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package typing
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
var dotsCSS = primitives.PrepareCSS(`
|
||||
@keyframes breathing {
|
||||
0% { opacity: 0.66; }
|
||||
100% { opacity: 0.12; }
|
||||
}
|
||||
|
||||
label {
|
||||
animation: breathing 800ms infinite alternate;
|
||||
}
|
||||
|
||||
label:nth-child(1) {
|
||||
animation-delay: 000ms;
|
||||
}
|
||||
|
||||
label:nth-child(2) {
|
||||
animation-delay: 150ms;
|
||||
}
|
||||
|
||||
label:nth-child(3) {
|
||||
animation-delay: 300ms;
|
||||
}
|
||||
`)
|
||||
|
||||
const breathingChar = "●"
|
||||
|
||||
func NewDots() *gtk.Box {
|
||||
c1, _ := gtk.LabelNew(breathingChar)
|
||||
c1.Show()
|
||||
c2, _ := gtk.LabelNew(breathingChar)
|
||||
c2.Show()
|
||||
c3, _ := gtk.LabelNew(breathingChar)
|
||||
c3.Show()
|
||||
|
||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
b.Add(c1)
|
||||
b.Add(c2)
|
||||
b.Add(c3)
|
||||
|
||||
primitives.AddClass(b, "breathing-dots")
|
||||
|
||||
primitives.AttachCSS(c1, dotsCSS)
|
||||
primitives.AttachCSS(c1, smallfonts)
|
||||
primitives.AttachCSS(c2, dotsCSS)
|
||||
primitives.AttachCSS(c2, smallfonts)
|
||||
primitives.AttachCSS(c3, dotsCSS)
|
||||
primitives.AttachCSS(c3, smallfonts)
|
||||
|
||||
return b
|
||||
}
|
145
internal/ui/messages/typing/state.go
Normal file
145
internal/ui/messages/typing/state.go
Normal 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
|
||||
}
|
118
internal/ui/messages/typing/typing.go
Normal file
118
internal/ui/messages/typing/typing.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package typing
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"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"
|
||||
)
|
||||
|
||||
var typingIndicatorCSS = primitives.PrepareCSS(`
|
||||
.typing-indicator {
|
||||
margin: 0 6px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
color: alpha(@theme_fg_color, 0.8);
|
||||
background-color: @theme_base_color;
|
||||
}
|
||||
`)
|
||||
|
||||
var smallfonts = primitives.PrepareCSS(`
|
||||
* { font-size: 0.9em; }
|
||||
`)
|
||||
|
||||
type Container struct {
|
||||
*gtk.Revealer
|
||||
state *State
|
||||
}
|
||||
|
||||
func New() *Container {
|
||||
d := NewDots()
|
||||
d.Show()
|
||||
|
||||
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, 4)
|
||||
b.PackStart(l, true, true, 0)
|
||||
b.Show()
|
||||
|
||||
r, _ := gtk.RevealerNew()
|
||||
r.SetTransitionDuration(50)
|
||||
r.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_CROSSFADE)
|
||||
r.SetRevealChild(false)
|
||||
r.Add(b)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
|
@ -13,6 +13,9 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/cozy"
|
||||
"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"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -37,7 +40,11 @@ type View struct {
|
|||
*sadface.FaceView
|
||||
Box *gtk.Box
|
||||
|
||||
Scroller *autoscroll.ScrolledWindow
|
||||
InputView *input.InputView
|
||||
|
||||
MsgBox *gtk.Box
|
||||
Typing *typing.Container
|
||||
Container container.Container
|
||||
contType int // msgIndex
|
||||
|
||||
|
@ -47,16 +54,36 @@ type View struct {
|
|||
|
||||
func NewView() *View {
|
||||
view := &View{}
|
||||
view.InputView = input.NewView(view)
|
||||
view.Typing = typing.New()
|
||||
view.Typing.Show()
|
||||
|
||||
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
view.Box.PackEnd(view.InputView, false, false, 0)
|
||||
view.Box.Show()
|
||||
view.MsgBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
||||
view.MsgBox.PackEnd(view.Typing, false, false, 0)
|
||||
view.MsgBox.Show()
|
||||
|
||||
// Create the message container, which will use PackEnd to add the widget on
|
||||
// TOP of the input view.
|
||||
// TOP of the typing indicator.
|
||||
view.createMessageContainer()
|
||||
|
||||
view.Scroller = autoscroll.NewScrolledWindow()
|
||||
view.Scroller.Add(view.MsgBox)
|
||||
view.Scroller.Show()
|
||||
|
||||
// A separator to go inbetween.
|
||||
sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL)
|
||||
sep.Show()
|
||||
|
||||
view.InputView = input.NewView(view)
|
||||
view.InputView.Show()
|
||||
|
||||
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
view.Box.PackStart(view.Scroller, true, true, 0)
|
||||
view.Box.PackStart(sep, false, false, 0)
|
||||
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()
|
||||
|
@ -68,7 +95,7 @@ func NewView() *View {
|
|||
func (v *View) createMessageContainer() {
|
||||
// Remove the old message container.
|
||||
if v.Container != nil {
|
||||
v.Box.Remove(v.Container)
|
||||
v.MsgBox.Remove(v.Container)
|
||||
}
|
||||
|
||||
// Update the container type.
|
||||
|
@ -80,15 +107,21 @@ func (v *View) createMessageContainer() {
|
|||
}
|
||||
|
||||
// Add the new message container.
|
||||
v.Box.PackEnd(v.Container, true, true, 0)
|
||||
v.MsgBox.PackEnd(v.Container, true, true, 0)
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
// Keep the scroller at the bottom.
|
||||
v.Scroller.Bottomed = true
|
||||
|
||||
// Recreate the message container if the type is different.
|
||||
if v.contType != msgIndex {
|
||||
v.createMessageContainer()
|
||||
|
@ -135,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
|
||||
})
|
||||
}
|
||||
|
@ -156,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)
|
||||
|
|
|
@ -48,7 +48,7 @@ func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
|
|||
input.Connect("key-press-event", KeyDownHandler(l, input.GrabFocus))
|
||||
|
||||
ibuf, _ := input.GetBuffer()
|
||||
ibuf.Connect("changed", func() {
|
||||
ibuf.Connect("end-user-action", func() {
|
||||
t, v := State(ibuf)
|
||||
c.Cursor = v
|
||||
c.Words, c.Index = split.SpaceIndexed(t, v)
|
||||
|
|
108
internal/ui/rich/async.go
Normal file
108
internal/ui/rich/async.go
Normal 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)})
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
Loading…
Reference in a new issue