mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-03-22 01:49:26 +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 (
|
require (
|
||||||
github.com/Xuanwo/go-locale v0.2.0
|
github.com/Xuanwo/go-locale v0.2.0
|
||||||
github.com/alecthomas/chroma v0.7.3
|
github.com/alecthomas/chroma v0.7.3
|
||||||
github.com/diamondburned/cchat v0.0.39
|
github.com/diamondburned/cchat v0.0.40
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200701182710-73a7393e7846
|
github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20200630025821-605d61d89288
|
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64
|
github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516
|
||||||
github.com/goodsign/monday v1.0.0
|
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/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194
|
||||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
|
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
|
||||||
github.com/ianlancetaylor/cgosymbolizer v0.0.0-20200424224625-be1b05b0b279
|
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/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 h1:1N8SGG2VNpLl6VVa8ueZm3Nm+dxvk8ffY9aviKHl4IE=
|
||||||
github.com/Xuanwo/go-locale v0.2.0/go.mod h1:6qbT9M726OJgyiGZro2YwPmx63wQzlH+VvtjJWQoftw=
|
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/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 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI=
|
||||||
github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
|
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/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/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/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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:P1ffsp+NHT22wWKYFVC8CdlGRLzPuUV9FcCBKOCJpCI=
|
||||||
github.com/diamondburned/arikawa v0.9.5/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
|
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.40 h1:38gPyJnnDoNDHrXcV8Qchfv3y6jlS3Fzz/6FY0BPH6I=
|
||||||
github.com/diamondburned/cchat v0.0.28/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat v0.0.40/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
github.com/diamondburned/cchat v0.0.31 h1:yUgrh5xbGX0R55glyxYtVewIDL2eXLJ+okIEfVaVoFk=
|
github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03 h1:F5TL7GPRU/D4ldVkS0haY3SiHPtf1Kby/4nbYpm//MQ=
|
||||||
github.com/diamondburned/cchat v0.0.31/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03/go.mod h1:p0X6QUH0mxK8yEW0+a4QA77ClAmoxz8CvgbnobMtWQA=
|
||||||
github.com/diamondburned/cchat v0.0.32 h1:nLiD4sL9+DLBnvNb9XLidd5peRzTgM9lWcqRsUmm474=
|
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3 h1:xr07/2cwINyrMqh92pQQJVDfQqG0u6gHAK+ZcGfpSew=
|
||||||
github.com/diamondburned/cchat v0.0.32/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3/go.mod h1:SRu3OOeggELFr2Wd3/+SpYV1eNcvSk2LBhM70NOZSG8=
|
||||||
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/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI=
|
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/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-20200704034004-40dbfc732516 h1:6j4oZahbNdVhSEInRfeYbgDpx1FXDfJy6CcUVyWOuVY=
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
|
github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516/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/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I=
|
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/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=
|
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-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/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/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.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
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=
|
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/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 h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
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/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
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/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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
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 h1:RFfyBPufP2V6cddUyyEVSHBpaAnM1WzaMNyqomeT+iY=
|
||||||
github.com/markbates/pkger v0.17.0/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
|
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.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/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 h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
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/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/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/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/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 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
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-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-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-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/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.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.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.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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
|
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package gts
|
package gts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -159,93 +158,21 @@ func ExecSync(fn func()) <-chan struct{} {
|
||||||
return ch
|
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 {
|
func EventIsRightClick(ev *gdk.Event) bool {
|
||||||
keyev := gdk.EventButtonNewFromEvent(ev)
|
keyev := gdk.EventButtonNewFromEvent(ev)
|
||||||
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
|
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.
|
// 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())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
connectDestroyer(img, cancel)
|
connectDestroyer(img, cancel)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package container
|
||||||
import (
|
import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"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/input"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
|
||||||
|
@ -47,7 +46,6 @@ type Container interface {
|
||||||
DeleteMessageUnsafe(cchat.MessageDelete)
|
DeleteMessageUnsafe(cchat.MessageDelete)
|
||||||
|
|
||||||
Reset()
|
Reset()
|
||||||
ScrollToBottom()
|
|
||||||
|
|
||||||
// AddPresendMessage adds and displays an unsent message.
|
// AddPresendMessage adds and displays an unsent message.
|
||||||
AddPresendMessage(msg input.PresendMessage) PresendGridMessage
|
AddPresendMessage(msg input.PresendMessage) PresendGridMessage
|
||||||
|
@ -59,6 +57,11 @@ type Container interface {
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
// BindMenu expects the controller to add actioner into the message.
|
// BindMenu expects the controller to add actioner into the message.
|
||||||
BindMenu(GridMessage)
|
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
|
// 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
|
// GridContainer is an implementation of Container, which allows flexible
|
||||||
// message grids.
|
// message grids.
|
||||||
type GridContainer struct {
|
type GridContainer struct {
|
||||||
*autoscroll.ScrolledWindow
|
|
||||||
*GridStore
|
*GridStore
|
||||||
|
Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// gridMessage w/ required internals
|
// gridMessage w/ required internals
|
||||||
|
@ -86,16 +89,9 @@ type gridMessage struct {
|
||||||
var _ Container = (*GridContainer)(nil)
|
var _ Container = (*GridContainer)(nil)
|
||||||
|
|
||||||
func NewGridContainer(constr Constructor, ctrl Controller) *GridContainer {
|
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{
|
return &GridContainer{
|
||||||
ScrolledWindow: sw,
|
GridStore: NewGridStore(constr, ctrl),
|
||||||
GridStore: store,
|
Controller: ctrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +102,7 @@ func (c *GridContainer) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||||
c.GridStore.CreateMessageUnsafe(msg)
|
c.GridStore.CreateMessageUnsafe(msg)
|
||||||
|
|
||||||
// Determine if the user is scrolled to the bottom for cleaning up.
|
// Determine if the user is scrolled to the bottom for cleaning up.
|
||||||
if !c.ScrolledWindow.Bottomed {
|
if !c.Bottomed() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,9 +132,3 @@ func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
|
||||||
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
|
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
gts.ExecAsync(func() { c.DeleteMessageUnsafe(msg) })
|
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
|
// Did the handler wipe old messages? It will only do so if the user is
|
||||||
// scrolled to the bottom.
|
// scrolled to the bottom.
|
||||||
if !c.ScrolledWindow.Bottomed {
|
if !c.Bottomed() {
|
||||||
// If we're not at the bottom, then we exit.
|
// If we're not at the bottom, then we exit.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type GridStore struct {
|
type GridStore struct {
|
||||||
Grid *gtk.Grid
|
*gtk.Grid
|
||||||
|
|
||||||
Construct Constructor
|
Construct Constructor
|
||||||
Controller Controller
|
Controller Controller
|
||||||
|
@ -26,7 +26,6 @@ func NewGridStore(constr Constructor, ctrl Controller) *GridStore {
|
||||||
grid.SetRowSpacing(5)
|
grid.SetRowSpacing(5)
|
||||||
grid.SetMarginStart(5)
|
grid.SetMarginStart(5)
|
||||||
grid.SetMarginEnd(5)
|
grid.SetMarginEnd(5)
|
||||||
grid.SetMarginBottom(5)
|
|
||||||
grid.Show()
|
grid.Show()
|
||||||
|
|
||||||
primitives.AddClass(grid, "message-grid")
|
primitives.AddClass(grid, "message-grid")
|
||||||
|
@ -230,6 +229,9 @@ func (c *GridStore) AddPresendMessage(msg input.PresendMessage) PresendGridMessa
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
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).
|
// Attempt to update before insertion (aka upsert).
|
||||||
if msgc := c.Message(msg); msgc != nil {
|
if msgc := c.Message(msg); msgc != nil {
|
||||||
msgc.UpdateAuthor(msg.Author())
|
msgc.UpdateAuthor(msg.Author())
|
||||||
|
@ -253,6 +255,9 @@ func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GridStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
|
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 msgc := c.Message(msg); msgc != nil {
|
||||||
if author := msg.Author(); author != nil {
|
if author := msg.Author(); author != nil {
|
||||||
msgc.UpdateAuthor(author)
|
msgc.UpdateAuthor(author)
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"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/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/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -20,16 +22,37 @@ type InputView struct {
|
||||||
Completer *completion.View
|
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 {
|
func NewView(ctrl Controller) *InputView {
|
||||||
text, _ := gtk.TextViewNew()
|
text, _ := gtk.TextViewNew()
|
||||||
text.SetSensitive(false)
|
text.SetSensitive(false)
|
||||||
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||||
text.SetProperty("top-margin", inputmargin)
|
text.SetVAlign(gtk.ALIGN_START)
|
||||||
text.SetProperty("left-margin", inputmargin)
|
text.SetProperty("top-margin", 4)
|
||||||
text.SetProperty("right-margin", inputmargin)
|
text.SetProperty("bottom-margin", 4)
|
||||||
text.SetProperty("bottom-margin", inputmargin)
|
text.SetProperty("left-margin", 8)
|
||||||
|
text.SetProperty("right-margin", 8)
|
||||||
text.Show()
|
text.Show()
|
||||||
|
|
||||||
|
primitives.AddClass(text, "message-input")
|
||||||
|
primitives.AttachCSS(text, textCSS)
|
||||||
|
|
||||||
// Bind the text event handler to text first.
|
// Bind the text event handler to text first.
|
||||||
c := completion.New(text)
|
c := completion.New(text)
|
||||||
|
|
||||||
|
@ -37,12 +60,7 @@ func NewView(ctrl Controller) *InputView {
|
||||||
f := NewField(text, ctrl)
|
f := NewField(text, ctrl)
|
||||||
f.Show()
|
f.Show()
|
||||||
|
|
||||||
// // Connect to the field's revealer. On resize, we want the autocompleter to
|
primitives.AddClass(f, "input-field")
|
||||||
// // 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 &InputView{f, c}
|
return &InputView{f, c}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +75,7 @@ func (v *InputView) SetSender(session cchat.Session, sender cchat.ServerMessageS
|
||||||
|
|
||||||
type Field struct {
|
type Field struct {
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
username *usernameContainer
|
Username *username.Container
|
||||||
|
|
||||||
TextScroll *gtk.ScrolledWindow
|
TextScroll *gtk.ScrolledWindow
|
||||||
text *gtk.TextView
|
text *gtk.TextView
|
||||||
|
@ -73,10 +91,14 @@ type Field struct {
|
||||||
editingID string // never empty
|
editingID string // never empty
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputmargin = 4
|
var scrollinputCSS = primitives.PrepareCSS(`
|
||||||
|
.scrolled-input {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||||
username := newUsernameContainer()
|
username := username.NewContainer()
|
||||||
username.Show()
|
username.Show()
|
||||||
|
|
||||||
buf, _ := text.GetBuffer()
|
buf, _ := text.GetBuffer()
|
||||||
|
@ -84,14 +106,18 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||||
sw := scrollinput.NewV(text, 150)
|
sw := scrollinput.NewV(text, 150)
|
||||||
sw.Show()
|
sw.Show()
|
||||||
|
|
||||||
|
primitives.AddClass(sw, "scrolled-input")
|
||||||
|
primitives.AttachCSS(sw, scrollinputCSS)
|
||||||
|
|
||||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
box.PackStart(username, false, false, 0)
|
box.PackStart(username, false, false, 0)
|
||||||
box.PackStart(sw, true, true, 0)
|
box.PackStart(sw, true, true, 0)
|
||||||
box.Show()
|
box.Show()
|
||||||
|
|
||||||
field := &Field{
|
field := &Field{
|
||||||
Box: box,
|
Box: box,
|
||||||
username: username,
|
Username: username,
|
||||||
|
// typing: typing,
|
||||||
TextScroll: sw,
|
TextScroll: sw,
|
||||||
text: text,
|
text: text,
|
||||||
buffer: buf,
|
buffer: buf,
|
||||||
|
@ -102,6 +128,13 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||||
text.SetFocusVAdjustment(sw.GetVAdjustment())
|
text.SetFocusVAdjustment(sw.GetVAdjustment())
|
||||||
text.Connect("key-press-event", field.keyDown)
|
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
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +146,7 @@ func (f *Field) Reset() {
|
||||||
f.UserID = ""
|
f.UserID = ""
|
||||||
f.Sender = nil
|
f.Sender = nil
|
||||||
f.editor = nil
|
f.editor = nil
|
||||||
f.username.Reset()
|
f.Username.Reset()
|
||||||
|
|
||||||
// reset the input
|
// reset the input
|
||||||
f.buffer.Delete(f.buffer.GetBounds())
|
f.buffer.Delete(f.buffer.GetBounds())
|
||||||
|
@ -123,7 +156,7 @@ func (f *Field) Reset() {
|
||||||
// disabled. Reset() should be called first.
|
// disabled. Reset() should be called first.
|
||||||
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
||||||
// Update the left username container in the input.
|
// Update the left username container in the input.
|
||||||
f.username.Update(session, sender)
|
f.Username.Update(session, sender)
|
||||||
f.UserID = session.ID()
|
f.UserID = session.ID()
|
||||||
|
|
||||||
// Set the sender.
|
// Set the sender.
|
||||||
|
|
|
@ -58,9 +58,9 @@ func (f *Field) sendInput() {
|
||||||
f.SendMessage(SendMessageData{
|
f.SendMessage(SendMessageData{
|
||||||
time: time.Now().UTC(),
|
time: time.Now().UTC(),
|
||||||
content: text,
|
content: text,
|
||||||
author: f.username.GetLabel(),
|
author: f.Username.GetLabel(),
|
||||||
authorID: f.UserID,
|
authorID: f.UserID,
|
||||||
authorURL: f.username.GetIconURL(),
|
authorURL: f.Username.GetIconURL(),
|
||||||
nonce: f.generateNonce(),
|
nonce: f.generateNonce(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package input
|
package username
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
"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-gtk/internal/ui/rich"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/diamondburned/imgutil"
|
"github.com/diamondburned/imgutil"
|
||||||
|
@ -23,7 +24,7 @@ func init() {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
type usernameContainer struct {
|
type Container struct {
|
||||||
*gtk.Revealer
|
*gtk.Revealer
|
||||||
main *gtk.Box
|
main *gtk.Box
|
||||||
avatar *rich.Icon
|
avatar *rich.Icon
|
||||||
|
@ -31,11 +32,17 @@ type usernameContainer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ cchat.LabelContainer = (*usernameContainer)(nil)
|
_ cchat.LabelContainer = (*Container)(nil)
|
||||||
_ cchat.IconContainer = (*usernameContainer)(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 := rich.NewIcon(AvatarSize, imgutil.Round(true))
|
||||||
avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
|
avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
|
||||||
avatar.Show()
|
avatar.Show()
|
||||||
|
@ -47,13 +54,12 @@ func newUsernameContainer() *usernameContainer {
|
||||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
|
||||||
box.PackStart(avatar, false, false, 0)
|
box.PackStart(avatar, false, false, 0)
|
||||||
box.PackStart(label, 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.SetVAlign(gtk.ALIGN_START)
|
||||||
box.Show()
|
box.Show()
|
||||||
|
|
||||||
|
primitives.AddClass(box, "username-view")
|
||||||
|
primitives.AttachCSS(box, usernameCSS)
|
||||||
|
|
||||||
rev, _ := gtk.RevealerNew()
|
rev, _ := gtk.RevealerNew()
|
||||||
rev.SetRevealChild(false)
|
rev.SetRevealChild(false)
|
||||||
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
||||||
|
@ -65,7 +71,7 @@ func newUsernameContainer() *usernameContainer {
|
||||||
// thread.
|
// thread.
|
||||||
currentRevealer = rev.SetRevealChild
|
currentRevealer = rev.SetRevealChild
|
||||||
|
|
||||||
return &usernameContainer{
|
return &Container{
|
||||||
Revealer: rev,
|
Revealer: rev,
|
||||||
main: box,
|
main: box,
|
||||||
avatar: avatar,
|
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.
|
// Only reveal if showUser is true.
|
||||||
u.Revealer.SetRevealChild(reveal && showUser)
|
u.Revealer.SetRevealChild(reveal && showUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldReveal returns whether or not the container should reveal.
|
// 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
|
return !u.label.GetLabel().Empty() && showUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *usernameContainer) Reset() {
|
func (u *Container) Reset() {
|
||||||
u.SetRevealChild(false)
|
u.SetRevealChild(false)
|
||||||
u.avatar.Reset()
|
u.avatar.Reset()
|
||||||
u.label.Reset()
|
u.label.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update is not thread-safe.
|
// 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.
|
// Set the fallback username.
|
||||||
u.label.SetLabelUnsafe(session.Name())
|
u.label.SetLabelUnsafe(session.Name())
|
||||||
// Reveal the name if it's not empty.
|
// 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.
|
// GetLabel is not thread-safe.
|
||||||
func (u *usernameContainer) GetLabel() text.Rich {
|
func (u *Container) GetLabel() text.Rich {
|
||||||
return u.label.GetLabel()
|
return u.label.GetLabel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLabel is thread-safe.
|
// SetLabel is thread-safe.
|
||||||
func (u *usernameContainer) SetLabel(content text.Rich) {
|
func (u *Container) SetLabel(content text.Rich) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
u.label.SetLabelUnsafe(content)
|
u.label.SetLabelUnsafe(content)
|
||||||
|
|
||||||
|
@ -123,7 +129,7 @@ func (u *usernameContainer) SetLabel(content text.Rich) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetIcon is thread-safe.
|
// SetIcon is thread-safe.
|
||||||
func (u *usernameContainer) SetIcon(url string) {
|
func (u *Container) SetIcon(url string) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
u.avatar.SetIconUnsafe(url)
|
u.avatar.SetIconUnsafe(url)
|
||||||
|
|
||||||
|
@ -136,6 +142,6 @@ func (u *usernameContainer) SetIcon(url string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIconURL is not thread-safe.
|
// GetIconURL is not thread-safe.
|
||||||
func (u *usernameContainer) GetIconURL() string {
|
func (u *Container) GetIconURL() string {
|
||||||
return u.avatar.URL()
|
return u.avatar.URL()
|
||||||
}
|
}
|
|
@ -81,7 +81,7 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
||||||
ts.SetXAlign(1) // right align
|
ts.SetXAlign(1) // right align
|
||||||
ts.SetVAlign(gtk.ALIGN_END)
|
ts.SetVAlign(gtk.ALIGN_END)
|
||||||
ts.SetSelectable(true)
|
// ts.SetSelectable(true)
|
||||||
ts.Show()
|
ts.Show()
|
||||||
|
|
||||||
user, _ := gtk.LabelNew("")
|
user, _ := gtk.LabelNew("")
|
||||||
|
@ -90,7 +90,7 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||||
user.SetXAlign(1) // right align
|
user.SetXAlign(1) // right align
|
||||||
user.SetVAlign(gtk.ALIGN_START)
|
user.SetVAlign(gtk.ALIGN_START)
|
||||||
user.SetSelectable(true)
|
// user.SetSelectable(true)
|
||||||
user.Show()
|
user.Show()
|
||||||
|
|
||||||
content, _ := gtk.LabelNew("")
|
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/container/cozy"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
"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/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/diamondburned/cchat-gtk/internal/ui/service/menu"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -37,7 +40,11 @@ type View struct {
|
||||||
*sadface.FaceView
|
*sadface.FaceView
|
||||||
Box *gtk.Box
|
Box *gtk.Box
|
||||||
|
|
||||||
|
Scroller *autoscroll.ScrolledWindow
|
||||||
InputView *input.InputView
|
InputView *input.InputView
|
||||||
|
|
||||||
|
MsgBox *gtk.Box
|
||||||
|
Typing *typing.Container
|
||||||
Container container.Container
|
Container container.Container
|
||||||
contType int // msgIndex
|
contType int // msgIndex
|
||||||
|
|
||||||
|
@ -47,16 +54,36 @@ type View struct {
|
||||||
|
|
||||||
func NewView() *View {
|
func NewView() *View {
|
||||||
view := &View{}
|
view := &View{}
|
||||||
view.InputView = input.NewView(view)
|
view.Typing = typing.New()
|
||||||
|
view.Typing.Show()
|
||||||
|
|
||||||
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
view.MsgBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
||||||
view.Box.PackEnd(view.InputView, false, false, 0)
|
view.MsgBox.PackEnd(view.Typing, false, false, 0)
|
||||||
view.Box.Show()
|
view.MsgBox.Show()
|
||||||
|
|
||||||
// Create the message container, which will use PackEnd to add the widget on
|
// 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.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
|
// placeholder logo
|
||||||
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256())
|
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256())
|
||||||
logo.Show()
|
logo.Show()
|
||||||
|
@ -68,7 +95,7 @@ func NewView() *View {
|
||||||
func (v *View) createMessageContainer() {
|
func (v *View) createMessageContainer() {
|
||||||
// Remove the old message container.
|
// Remove the old message container.
|
||||||
if v.Container != nil {
|
if v.Container != nil {
|
||||||
v.Box.Remove(v.Container)
|
v.MsgBox.Remove(v.Container)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the container type.
|
// Update the container type.
|
||||||
|
@ -80,15 +107,21 @@ func (v *View) createMessageContainer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new message container.
|
// 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() {
|
func (v *View) Reset() {
|
||||||
v.state.Reset() // Reset the state variables.
|
v.state.Reset() // Reset the state variables.
|
||||||
|
v.Typing.Reset() // Reset the typing state.
|
||||||
v.FaceView.Reset() // Switch back to the main screen.
|
v.FaceView.Reset() // Switch back to the main screen.
|
||||||
v.InputView.Reset() // Reset the input.
|
v.InputView.Reset() // Reset the input.
|
||||||
v.Container.Reset() // Clean all messages.
|
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.
|
// Recreate the message container if the type is different.
|
||||||
if v.contType != msgIndex {
|
if v.contType != msgIndex {
|
||||||
v.createMessageContainer()
|
v.createMessageContainer()
|
||||||
|
@ -135,6 +168,8 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, done func
|
||||||
// Set the cancel handler.
|
// Set the cancel handler.
|
||||||
v.state.setcurrent(s)
|
v.state.setcurrent(s)
|
||||||
|
|
||||||
|
// Try setting the typing indicator if available.
|
||||||
|
v.Typing.TrySubscribe(server)
|
||||||
}, nil
|
}, 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.
|
// LatestMessageFrom returns the last message ID with that author.
|
||||||
func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
func (v *View) LatestMessageFrom(userID string) (msgID string, ok bool) {
|
||||||
return v.Container.LatestMessageFrom(userID)
|
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))
|
input.Connect("key-press-event", KeyDownHandler(l, input.GrabFocus))
|
||||||
|
|
||||||
ibuf, _ := input.GetBuffer()
|
ibuf, _ := input.GetBuffer()
|
||||||
ibuf.Connect("changed", func() {
|
ibuf.Connect("end-user-action", func() {
|
||||||
t, v := State(ibuf)
|
t, v := State(ibuf)
|
||||||
c.Cursor = v
|
c.Cursor = v
|
||||||
c.Words, c.Index = split.SpaceIndexed(t, 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 {
|
type Icon struct {
|
||||||
*gtk.Revealer
|
*gtk.Revealer
|
||||||
Image *gtk.Image
|
Image *gtk.Image
|
||||||
resizer imgutil.Processor
|
procs []imgutil.Processor
|
||||||
procs []imgutil.Processor
|
size int
|
||||||
size int
|
|
||||||
|
|
||||||
r gts.Reusable
|
r *Reusable
|
||||||
|
|
||||||
// state
|
// state
|
||||||
url string
|
url string
|
||||||
|
@ -50,10 +49,11 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
|
||||||
Revealer: rev,
|
Revealer: rev,
|
||||||
Image: img,
|
Image: img,
|
||||||
procs: procs,
|
procs: procs,
|
||||||
|
|
||||||
r: *gts.NewReusable(),
|
|
||||||
}
|
}
|
||||||
i.SetSize(sizepx)
|
i.SetSize(sizepx)
|
||||||
|
i.r = NewReusable(func(ni *nullIcon) {
|
||||||
|
i.SetIconUnsafe(ni.url)
|
||||||
|
})
|
||||||
|
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,7 @@ func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
|
||||||
// Reset wipes the state to be just after construction.
|
// Reset wipes the state to be just after construction.
|
||||||
func (i *Icon) Reset() {
|
func (i *Icon) Reset() {
|
||||||
i.url = ""
|
i.url = ""
|
||||||
|
i.r.Invalidate() // invalidate async fetching images
|
||||||
i.Revealer.SetRevealChild(false)
|
i.Revealer.SetRevealChild(false)
|
||||||
i.Image.SetFromPixbuf(nil) // destroy old pb
|
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) {
|
func (i *Icon) SetSize(szpx int) {
|
||||||
i.size = szpx
|
i.size = szpx
|
||||||
i.Image.SetSizeRequest(szpx, szpx)
|
i.Image.SetSizeRequest(szpx, szpx)
|
||||||
i.resizer = imgutil.Resize(szpx, szpx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddProcessors is not thread-safe.
|
// AddProcessors is not thread-safe.
|
||||||
|
@ -112,16 +112,11 @@ func (i *Icon) SetIcon(url string) {
|
||||||
gts.ExecAsync(func() { i.SetIconUnsafe(url) })
|
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) {
|
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{}
|
ni := &nullIcon{}
|
||||||
f, err := iconer.Icon(ctx, ni)
|
f, err := iconer.Icon(ctx, ni)
|
||||||
ni.cancel = f
|
return ni, f, err
|
||||||
return ni, err
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +128,11 @@ func (i *Icon) SetIconUnsafe(url string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Icon) updateAsync() {
|
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 {
|
type ToggleButtonImage struct {
|
||||||
|
|
|
@ -30,7 +30,7 @@ type Label struct {
|
||||||
current text.Rich
|
current text.Rich
|
||||||
|
|
||||||
// Reusable primitive.
|
// Reusable primitive.
|
||||||
r gts.Reusable
|
r *Reusable
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -44,12 +44,17 @@ func NewLabel(content text.Rich) *Label {
|
||||||
label.SetXAlign(0) // left align
|
label.SetXAlign(0) // left align
|
||||||
label.SetEllipsize(pango.ELLIPSIZE_END)
|
label.SetEllipsize(pango.ELLIPSIZE_END)
|
||||||
|
|
||||||
return &Label{
|
l := &Label{
|
||||||
Label: *label,
|
Label: *label,
|
||||||
current: content,
|
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.
|
// Reset wipes the state to be just after construction.
|
||||||
|
@ -59,17 +64,11 @@ func (l *Label) Reset() {
|
||||||
l.Label.SetText("")
|
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) {
|
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{}
|
nl := &nullLabel{}
|
||||||
f, err := fn(ctx, nl)
|
f, err := fn(ctx, nl)
|
||||||
nl.cancel = f
|
return nl, f, err
|
||||||
return nl, err
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,15 +17,13 @@ func MakeRed(content text.Rich) string {
|
||||||
// used for grabbing text without changing state
|
// used for grabbing text without changing state
|
||||||
type nullLabel struct {
|
type nullLabel struct {
|
||||||
text.Rich
|
text.Rich
|
||||||
cancel func()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t }
|
func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t }
|
||||||
|
|
||||||
// used for grabbing url without changing state
|
// used for grabbing url without changing state
|
||||||
type nullIcon struct {
|
type nullIcon struct {
|
||||||
url string
|
url string
|
||||||
cancel func()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *nullIcon) SetIcon(url string) { i.url = url }
|
func (i *nullIcon) SetIcon(url string) { i.url = url }
|
||||||
|
|
|
@ -21,11 +21,18 @@ type View struct {
|
||||||
Services []*Container
|
Services []*Container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var servicesCSS = primitives.PrepareCSS(`
|
||||||
|
.services {
|
||||||
|
background-color: @theme_base_color;
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
func NewView() *View {
|
func NewView() *View {
|
||||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
box.Show()
|
box.Show()
|
||||||
|
|
||||||
primitives.AddClass(box, "services")
|
primitives.AddClass(box, "services")
|
||||||
|
primitives.AttachCSS(box, servicesCSS)
|
||||||
|
|
||||||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
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 {
|
type Session struct {
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
|
|
||||||
|
@ -101,6 +105,8 @@ func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
|
||||||
primitives.AddClass(b, "commander")
|
primitives.AddClass(b, "commander")
|
||||||
primitives.AddClass(view, "command-buffer")
|
primitives.AddClass(view, "command-buffer")
|
||||||
primitives.AddClass(input, "command-input")
|
primitives.AddClass(input, "command-input")
|
||||||
|
primitives.AttachCSS(view, commandPadding)
|
||||||
|
primitives.AttachCSS(input, commandPadding)
|
||||||
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
|
/* Make CSS more consistent across themes */
|
||||||
headerbar { padding: 0; }
|
headerbar { padding: 0; }
|
||||||
|
|
||||||
|
/* Consistent design */
|
||||||
.services button { border-radius: 0; }
|
.services button { border-radius: 0; }
|
||||||
|
|
||||||
.message-content, .message-content text {
|
popover > box { margin: 6px; }
|
||||||
background: none;
|
|
||||||
box-shadow: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
popover > box {
|
/* Hack to fix the input bar being high in Adwaita */
|
||||||
margin: 6px;
|
.input-field * { min-height: 0; }
|
||||||
}
|
|
||||||
|
|
||||||
.command-buffer, .command-input {
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue