From 50376cb2b025eaecb0d409e7ca3348f9f1c06662 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Tue, 29 Dec 2020 22:30:41 -0800 Subject: [PATCH] update cchat-gtk to latest gotk3 to fix leaks --- go.mod | 17 +- go.sum | 26 +++ icons/icons.go | 29 ++- internal/gts/gts.go | 66 ++----- internal/gts/httputil/httputil.go | 36 +--- internal/gts/httputil/image.go | 177 ++++++++++++------ internal/gts/throttler/throttler.go | 6 +- internal/ui/config/preferences/preferences.go | 2 +- internal/ui/config/widgets.go | 6 +- internal/ui/dialog/dialog.go | 6 +- .../messages/container/cozy/message_full.go | 4 +- internal/ui/messages/header.go | 6 +- .../messages/input/attachment/attachment.go | 165 ++++++++-------- .../ui/messages/input/attachment/progress.go | 8 +- internal/ui/messages/input/input.go | 15 +- internal/ui/messages/input/keydown.go | 22 +-- internal/ui/messages/memberlist/memberlist.go | 6 +- internal/ui/messages/typing/typing.go | 2 +- internal/ui/messages/view.go | 22 +-- internal/ui/primitives/actions/menubutton.go | 2 +- .../primitives/buttonoverlay/buttonoverlay.go | 4 - .../ui/primitives/completion/completer.go | 9 +- internal/ui/primitives/drag/drag.go | 6 +- internal/ui/primitives/menu/menu.go | 4 +- internal/ui/primitives/primitives.go | 23 ++- internal/ui/primitives/roundimage/avatar.go | 12 +- .../ui/primitives/roundimage/roundimage.go | 4 +- internal/ui/primitives/roundimage/static.go | 10 +- internal/ui/rich/image.go | 2 +- internal/ui/rich/label.go | 2 +- internal/ui/rich/labeluri/labeluri.go | 8 +- internal/ui/rich/parser/attrmap/attrmap.go | 4 +- internal/ui/rich/parser/markup/markup.go | 30 ++- internal/ui/service/auth/auth.go | 2 +- internal/ui/service/config/widgets.go | 2 +- internal/ui/service/header.go | 4 +- internal/ui/service/list.go | 1 + internal/ui/service/service.go | 23 ++- .../service/session/server/button/button.go | 11 +- .../ui/service/session/server/children.go | 102 +++++----- internal/ui/service/session/server/server.go | 7 +- internal/ui/service/session/servers.go | 32 +++- internal/ui/service/session/session.go | 35 ++-- internal/ui/service/view.go | 2 + internal/ui/ui.go | 12 +- madvdontneed.go | 16 -- main.go | 18 ++ shell.nix | 44 +++-- 48 files changed, 606 insertions(+), 446 deletions(-) diff --git a/go.mod b/go.mod index 21fdd62..f4e2482 100644 --- a/go.mod +++ b/go.mod @@ -2,22 +2,21 @@ module github.com/diamondburned/cchat-gtk go 1.14 -replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4 +replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385 -//replace github.com/diamondburned/cchat-discord => ../cchat-discord - -//replace github.com/diamondburned/ningen/v2 => ../../ningen - -//replace github.com/diamondburned/arikawa/v2 => ../../arikawa +// replace github.com/diamondburned/gotk3-tcmalloc => ../../gotk3-tcmalloc +// replace github.com/diamondburned/cchat-discord => ../cchat-discord +// replace github.com/diamondburned/ningen/v2 => ../../ningen +// replace github.com/diamondburned/arikawa/v2 => ../../arikawa require ( github.com/Xuanwo/go-locale v1.0.0 github.com/alecthomas/chroma v0.7.3 github.com/diamondburned/cchat v0.3.15 - github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af + github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092 github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db - github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894 - github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4 + github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828 + github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374 github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 github.com/disintegration/imaging v1.6.2 github.com/goodsign/monday v1.0.0 diff --git a/go.sum b/go.sum index 0b93150..54c9f54 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/diamondburned/arikawa/v2 v2.0.0-20201219075756-36c2f166becd h1:HCaw0Y github.com/diamondburned/arikawa/v2 v2.0.0-20201219075756-36c2f166becd/go.mod h1:/vapSS3yfYRAt5hhgI6JiPkca+wKhgi0MdanT1dBBQY= github.com/diamondburned/arikawa/v2 v2.0.0-20201220032235-088b30430377 h1:71BLnECSl0/Ns7iZmEm7MpE5+qSuWw/BQBQY2XCUmVc= github.com/diamondburned/arikawa/v2 v2.0.0-20201220032235-088b30430377/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0= +github.com/diamondburned/arikawa/v2 v2.0.0-20201227001310-f3f075b27f44 h1:i6Jec7bvVY8NhwW3L0SlpfWM6r2p2i67XuhiOEzkfwI= +github.com/diamondburned/arikawa/v2 v2.0.0-20201227001310-f3f075b27f44/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0= github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJEpCHg= github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= github.com/diamondburned/cchat v0.3.15 h1:BJf8ZiRtDWTGMtQ3QqjNU0H+784WSrkJEpFGkKY5gEw= @@ -60,6 +62,10 @@ github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d h1:n61 github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms= github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af h1:pTdxsrVSYCdraGormbu1t8uQJMe/OD/ZIz9KljDWAvc= github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms= +github.com/diamondburned/cchat-discord v0.0.0-20201227023505-c4e360010fb8 h1:eyK9GRaHg1KcWZx4hBPHWG16+EwgZi5groEW5I/FRq0= +github.com/diamondburned/cchat-discord v0.0.0-20201227023505-c4e360010fb8/go.mod h1:i3y8dyAFrtigpGOwunBdoJK/phwt9Gp/wfpVJb4imV0= +github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092 h1:oxY7APUclLgaWjaTK++7kHBdl0GdVyqOvHQv68TcpHw= +github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092/go.mod h1:rFBGZYLq0g6Pb/WGN/K0++kXrhCYlQQ1nc2FX4r8CO0= github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg= github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc= github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw= @@ -70,16 +76,36 @@ github.com/diamondburned/gotk3 v0.0.0-20201221091325-c5152a10909f h1:Lnrq+vXBgzb github.com/diamondburned/gotk3 v0.0.0-20201221091325-c5152a10909f/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4 h1:KvlmpqxLoXKg+j5uiJWZXhacfgPg4fi/8wLecWX+XlE= github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= +github.com/diamondburned/gotk3 v0.0.0-20201225090124-444dc90054da h1:ovty7leKv+E6PqocAeK+toLYnaMqbhZX69oPFNQK5Fk= +github.com/diamondburned/gotk3 v0.0.0-20201225090124-444dc90054da/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= +github.com/diamondburned/gotk3 v0.0.0-20201226041445-1e6de5f7c2b2 h1:ExJxwhfSnIRJxL+CdcRZM33xoJ8WycoXGw6z3LaBFVM= +github.com/diamondburned/gotk3 v0.0.0-20201226041445-1e6de5f7c2b2/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= +github.com/diamondburned/gotk3 v0.0.0-20201226085844-7ac29c072f03 h1:/azfkq4zFt8JPo8p2c/7kJJcf4cgTH1OaigR3fZHvIA= +github.com/diamondburned/gotk3 v0.0.0-20201226085844-7ac29c072f03/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= +github.com/diamondburned/gotk3 v0.0.0-20201229054305-848200601f20 h1:/jSna2cSHrNXSzR9rNp9kUQRd/VOC/FL7vhRBor4zOc= +github.com/diamondburned/gotk3 v0.0.0-20201229054305-848200601f20/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= +github.com/diamondburned/gotk3 v0.0.0-20201229092333-f5c9db5d1d59 h1:9nYQE9MZDcu++bFyyxKUQdin4ML+0PRoRm0bIUhjuBs= +github.com/diamondburned/gotk3 v0.0.0-20201229092333-f5c9db5d1d59/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= +github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385 h1:nmlMCeEWmT6z9GAslxkTBZd9mH0Yt2QtWFD1yxGCJ98= +github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894 h1:QgI21deaQbCUMnxKkQQUXzQolnAe1dMIXAWwqAyOp2g= github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894/go.mod h1:IoyMxPKSJOMoP0BiBuFwf2RDMeA4Uqx0HPKN5BzqTtA= +github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828 h1:Lm1F+GwrDdAaaMzrR7AYl4GGd/T+FE2OgOz25QWwsIg= +github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828/go.mod h1:ODW0Ai5dTVVp/HNUSSDTl5qE6K642CoOSZ8isIhupNg= github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4 h1:qF5VHC35+GyCjUmKz+1O94xpFc0JQd4Ui3h+I955pJw= github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4/go.mod h1:V0qyhW4v6KPFwtDpXdBm5aWH7zWEyrzZpcB6MPnKArQ= +github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374 h1:1KLPz5mbYF7t3ajrK55alkDcbjWc+7aQFjKzV9qJRN4= +github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374/go.mod h1:9EiMOAKWhEFoVnFLTco2v0v0rJn855YAHDDa2bL8urU= github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 h1:OWxllHbUptXzDias6YI4MM0R3o50q8MfhkkwVIlfiNo= github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ= github.com/diamondburned/ningen/v2 v2.0.0-20201219070301-15610044db9a h1:w8CWPYiwH9p2XGlHHeTqRWx7e8CJJLN8i4orAkOa27Y= github.com/diamondburned/ningen/v2 v2.0.0-20201219070301-15610044db9a/go.mod h1:Pw4ZPQmZUonCytlKhHgan98CZeCQ4AWh0DWqvnhsuNE= github.com/diamondburned/ningen/v2 v2.0.0-20201220054153-c69c4f7057b4 h1:qzh5ghfgvUllilOhkrGP29IGQT6DGfcc3lhk9uSA6nU= github.com/diamondburned/ningen/v2 v2.0.0-20201220054153-c69c4f7057b4/go.mod h1:2ZjyeHqO9jCdlfmJhhVhk8eCumx418n39uVaC/LgEgY= +github.com/diamondburned/ningen/v2 v2.0.0-20201227020621-a4e33db11d3c h1:IWn/N54JkJz7PVgmAt7cWXLky9wY0UjXzznNoWLIFgA= +github.com/diamondburned/ningen/v2 v2.0.0-20201227020621-a4e33db11d3c/go.mod h1:zQkAo1RT4ru4HW6B5T4IRO2pee8ITzTUA2Y7XNpgjqo= +github.com/diamondburned/ningen/v2 v2.0.0-20201227034843-dc1d22fc28e4 h1:ptIpcyB/FIBM5viKLVXuBiE1utqBcH4S3l5kUQrsL9Q= +github.com/diamondburned/ningen/v2 v2.0.0-20201227034843-dc1d22fc28e4/go.mod h1:zQkAo1RT4ru4HW6B5T4IRO2pee8ITzTUA2Y7XNpgjqo= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= diff --git a/icons/icons.go b/icons/icons.go index 43869f9..31a6810 100644 --- a/icons/icons.go +++ b/icons/icons.go @@ -3,28 +3,43 @@ package icons import ( "log" + "github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/gdk" ) // static assets -var assets = map[string]*gdk.Pixbuf{} +// var assets = map[string]*gdk.Pixbuf{} -func Logo256Variant2(sz int) *gdk.Pixbuf { - return loadPixbuf(__cchat_variant2_256, sz) +func Logo256Variant2(sz, scale int) *cairo.Surface { + return mustSurface(loadPixbuf(__cchat_variant2_256, sz, scale), scale) } -func Logo256(sz int) *gdk.Pixbuf { - return loadPixbuf(__cchat_256, sz) +func Logo256(sz, scale int) *cairo.Surface { + return mustSurface(loadPixbuf(__cchat_256, sz, scale), scale) } -func loadPixbuf(data []byte, sz int) *gdk.Pixbuf { +func Logo256Pixbuf() *gdk.Pixbuf { + return loadPixbuf(__cchat_256, 256, 1) +} + +func mustSurface(p *gdk.Pixbuf, scale int) *cairo.Surface { + surface, err := gdk.CairoSurfaceCreateFromPixbuf(p, scale, nil) + if err != nil { + log.Fatalln("Failed to create surface from pixbuf:", err) + } + return surface +} + +func loadPixbuf(data []byte, sz, scale int) *gdk.Pixbuf { l, err := gdk.PixbufLoaderNew() if err != nil { log.Fatalln("Failed to create a pixbuf loader for icons:", err) } if sz > 0 { - l.Connect("size-prepared", func() { l.SetSize(sz, sz) }) + l.Connect("size-prepared", func(l *gdk.PixbufLoader) { + l.SetSize(sz*scale, sz*scale) + }) } p, err := l.WriteAndReturnPixbuf(data) diff --git a/internal/gts/gts.go b/internal/gts/gts.go index 63b3809..47b6fbe 100644 --- a/internal/gts/gts.go +++ b/internal/gts/gts.go @@ -1,15 +1,12 @@ package gts import ( - "fmt" - "image" "os" "time" "github.com/diamondburned/cchat-gtk/internal/gts/throttler" "github.com/diamondburned/cchat-gtk/internal/log" "github.com/diamondburned/handy" - "github.com/disintegration/imaging" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" @@ -66,15 +63,14 @@ func NewEmptyModalDialog() (*gtk.Dialog, error) { if err != nil { return nil, errors.Wrap(err, "Failed to get content area") } - - d.Remove(b) + b.Destroy() return d, nil } func AddAppAction(name string, call func()) { action := glib.SimpleActionNew(name, nil) - action.Connect("activate", call) + action.Connect("activate", func(*glib.SimpleAction) { call() }) App.AddAction(action) } @@ -90,12 +86,12 @@ func init() { type MainApplication interface { gtk.IWidget Menu() *glib.MenuModel - Icon() *gdk.Pixbuf + Icon() *gdk.Pixbuf // assume scale 1 Close() } func Main(wfn func() MainApplication) { - App.Application.Connect("activate", func() { + App.Application.Connect("activate", func(*gtk.Application) { handy.Init() // Load all CSS onto the default screen. @@ -119,17 +115,16 @@ func Main(wfn func() MainApplication) { w := wfn() App.Window.Add(w) App.Window.SetIcon(w.Icon()) - // App.Application.SetAppMenu(w.Menu()) // Connect the destructor. - App.Window.Window.Connect("destroy", func() { + App.Window.Window.Connect("destroy", func(window *handy.ApplicationWindow) { // Hide the application window. - App.Window.Hide() + window.Hide() // Let the main loop run once by queueing the stop loop afterwards. // This is to allow the main loop to properly hide the Gtk window // before trying to disconnect. - ExecAsync(func() { + ExecLater(func() { // Stop the application loop. App.Application.Quit() // Finalize the application by running the closer. @@ -164,9 +159,14 @@ func Async(fn func() (func(), error)) { }() } +// ExecLater executes the function asynchronously with a low priority. +func ExecLater(fn func()) { + glib.IdleAddPriority(glib.PRIORITY_LOW, fn) +} + // ExecAsync executes function asynchronously in the Gtk main thread. func ExecAsync(fn func()) { - glib.IdleAdd(fn) + glib.IdleAddPriority(glib.PRIORITY_HIGH, fn) } // ExecSync executes the function asynchronously, but returns a channel that @@ -174,7 +174,7 @@ func ExecAsync(fn func()) { func ExecSync(fn func()) <-chan struct{} { var ch = make(chan struct{}) - glib.IdleAdd(func() { + glib.IdleAddPriority(glib.PRIORITY_HIGH, func() { fn() close(ch) }) @@ -189,10 +189,7 @@ func DoAfter(d time.Duration, f func()) { // DoAfterMs calls f after the given ms in the Gtk main loop. func DoAfterMs(ms uint, f func()) { - _, err := glib.TimeoutAdd(ms, f) - if err != nil { - panic(err) - } + glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, f) } // AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk @@ -203,11 +200,7 @@ func AfterFunc(d time.Duration, f func()) (stop func()) { // AfterMsFunc is similar to AfterFunc but takes in milliseconds instead. func AfterMsFunc(ms uint, f func()) (stop func()) { - h, err := glib.TimeoutAdd(ms, func() bool { f(); return true }) - if err != nil { - panic(err) - } - + h := glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, func() bool { f(); return true }) return func() { glib.SourceRemove(h) } } @@ -216,30 +209,6 @@ func EventIsRightClick(ev *gdk.Event) bool { return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY } -func RenderPixbuf(img image.Image) *gdk.Pixbuf { - var nrgba *image.NRGBA - if n, ok := img.(*image.NRGBA); ok { - nrgba = n - } else { - nrgba = imaging.Clone(img) - } - - pix, err := gdk.PixbufNewFromData( - nrgba.Pix, gdk.COLORSPACE_RGB, - true, // NRGBA has alpha. - 8, // 8-bit aka 1-byte per sample. - nrgba.Rect.Dx(), - nrgba.Rect.Dy(), // We already know the image size. - nrgba.Stride, - ) - - if err != nil { - panic(fmt.Sprintf("Failed to create pixbuf from *NRGBA: %v", err)) - } - - return pix -} - func SpawnUploader(dirpath string, callback func(absolutePaths []string)) { dialog, _ := gtk.FileChooserNativeDialogNew( "Upload File", App.Window, @@ -276,7 +245,7 @@ func BindPreviewer(fc *gtk.FileChooserNativeDialog) { fc.SetPreviewWidget(img) fc.Connect("update-preview", - func(_ interface{}, img *gtk.Image) { + func(fc *gtk.FileChooserNativeDialog) { file := fc.GetPreviewFilename() b, err := gdk.PixbufNewFromFileAtScale(file, 256, 256, true) @@ -288,6 +257,5 @@ func BindPreviewer(fc *gtk.FileChooserNativeDialog) { img.SetFromPixbuf(b) fc.SetPreviewWidgetActive(true) }, - img, ) } diff --git a/internal/gts/httputil/httputil.go b/internal/gts/httputil/httputil.go index f9b4525..b721d31 100644 --- a/internal/gts/httputil/httputil.go +++ b/internal/gts/httputil/httputil.go @@ -2,20 +2,18 @@ package httputil import ( "context" - "io" "net/http" "os" "path/filepath" "time" - "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/gregjones/httpcache" "github.com/gregjones/httpcache/diskcache" "github.com/peterbourgon/diskv" "github.com/pkg/errors" ) -var basePath = filepath.Join(os.TempDir(), "cchat-gtk-sabotaging-the-desktop-experience") +var basePath = filepath.Join(os.TempDir(), "cchat-gtk-totally-not-node-modules") var dskcached = http.Client{ Timeout: 15 * time.Second, @@ -25,40 +23,12 @@ var dskcached = http.Client{ TempDir: filepath.Join(basePath, "tmp"), PathPerm: 0750, FilePerm: 0750, - Compression: diskv.NewZlibCompressionLevel(2), - CacheSizeMax: 25 * 1024 * 1024, // 25 MiB in memory + Compression: diskv.NewZlibCompressionLevel(5), + CacheSizeMax: 0, // 25 MiB in memory })), ), } -func AsyncStreamUncached(url string, fn func(r io.Reader)) { - gts.Async(func() (func(), error) { - r, err := get(context.Background(), url, false) - if err != nil { - return nil, err - } - - return func() { - fn(r.Body) - r.Body.Close() - }, nil - }) -} - -func AsyncStream(url string, fn func(r io.Reader)) { - gts.Async(func() (func(), error) { - r, err := get(context.Background(), url, true) - if err != nil { - return nil, err - } - - return func() { - fn(r.Body) - r.Body.Close() - }, nil - }) -} - func get(ctx context.Context, url string, cached bool) (r *http.Response, err error) { q, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { diff --git a/internal/gts/httputil/image.go b/internal/gts/httputil/image.go index 058ac49..0ded0e2 100644 --- a/internal/gts/httputil/image.go +++ b/internal/gts/httputil/image.go @@ -3,6 +3,10 @@ package httputil import ( "context" "io" + "mime" + "net/http" + "net/url" + "path" "strings" "github.com/diamondburned/cchat-gtk/internal/gts" @@ -41,7 +45,7 @@ type surfaceWrapper struct { scale int } -func (wrapper surfaceWrapper) SetFromPixbuf(pb *gdk.Pixbuf) { +func (wrapper *surfaceWrapper) SetFromPixbuf(pb *gdk.Pixbuf) { surface, _ := gdk.CairoSurfaceCreateFromPixbuf(pb, wrapper.scale, nil) wrapper.SetFromSurface(surface) } @@ -49,98 +53,149 @@ func (wrapper surfaceWrapper) SetFromPixbuf(pb *gdk.Pixbuf) { // AsyncImage loads an image. This method uses the cache. It prefers loading // SetFromSurface over SetFromPixbuf, but will fallback if needed be. func AsyncImage(ctx context.Context, - img ImageContainer, url string, procs ...imgutil.Processor) { + img ImageContainer, imageURL string, procs ...imgutil.Processor) { - if url == "" { - return - } - - gif := strings.Contains(url, ".gif") - scale := 1 - - surfaceContainer, canSurface := img.(SurfaceContainer) - - if canSurface = canSurface && !gif; canSurface { - // Only bother with this API if we even have HiDPI. - if scale = surfaceContainer.GetScaleFactor(); scale > 1 { - img = surfaceWrapper{surfaceContainer, scale} - } - } - - ctx = primitives.HandleDestroyCtx(ctx, img) - - l, err := gdk.PixbufLoaderNew() - if err != nil { - log.Error(errors.Wrap(err, "Failed to make pixbuf loader")) + if imageURL == "" { return } w, h := img.GetSizeRequest() - l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) { - w, h = imgutil.MaxSize(imgW, imgH, w, h) - if w != imgW || h != imgH || scale > 1 { - l.SetSize(w*scale, h*scale) + scale := 1 + + surfaceContainer, canSurface := img.(SurfaceContainer) + if canSurface { + scale = surfaceContainer.GetScaleFactor() + } + + go func() { + ctx := primitives.HandleDestroyCtx(ctx, img) + + // Try and guess the MIME type from the URL. + mimeType := mime.TypeByExtension(urlExt(imageURL)) + + r, err := get(ctx, imageURL, true) + if err != nil { + log.Error(errors.Wrap(err, "failed to GET")) + return } - }) + defer r.Body.Close() - l.Connect("area-prepared", areaPreparedFn(ctx, img, gif)) + // Try and use the image type from the MIME header over the type from + // the URL, as it is more reliable. + if mime := mimeFromHeaders(r.Header); mime != "" { + mimeType = mime + } - go downloadImage(ctx, l, url, procs, gif) + _, fileType := path.Split(mimeType) // abuse split "a/b" to get b + + isGIF := fileType == "gif" + if isGIF { + canSurface = false + scale = 1 + } + + // Only bother with this if we even have HiDPI. We also can't use a + // Surface for a GIF. + if canSurface && scale > 1 { + img = &surfaceWrapper{surfaceContainer, scale} + } + + l, err := gdk.PixbufLoaderNewWithType(fileType) + if err != nil { + log.Error(errors.Wrap(err, "failed to make pixbuf loader")) + return + } + + l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) { + w, h = imgutil.MaxSize(imgW, imgH, w, h) + if w != imgW || h != imgH || scale > 1 { + l.SetSize(w*scale, h*scale) + } + }) + + load := loadFn(ctx, img, isGIF) + l.Connect("area-prepared", load) + l.Connect("area-updated", load) + + if err := downloadImage(r.Body, l, procs, isGIF); err != nil { + log.Error(errors.Wrapf(err, "failed to download %q", imageURL)) + // Force close after downloading. + } + + if err := l.Close(); err != nil { + log.Error(errors.Wrapf(err, "failed to close pixbuf loader for %q", imageURL)) + } + }() } -func areaPreparedFn(ctx context.Context, img ImageContainer, gif bool) func(l *gdk.PixbufLoader) { +func urlExt(anyURL string) string { + u, err := url.Parse(anyURL) + if err != nil { + return path.Ext(strings.SplitN(anyURL, "?", 1)[0]) + } + + return path.Ext(u.Path) +} + +func mimeFromHeaders(headers http.Header) string { + cType := headers.Get("Content-Type") + if cType == "" { + return "" + } + media, _, err := mime.ParseMediaType(cType) + if err != nil { + return "" + } + return media +} + +func loadFn(ctx context.Context, img ImageContainer, isGIF bool) func(l *gdk.PixbufLoader) { + var pixbuf interface{} + return func(l *gdk.PixbufLoader) { - if !gif { - p, err := l.GetPixbuf() - if err != nil { - log.Error(errors.Wrap(err, "Failed to get pixbuf")) - return + if pixbuf == nil { + if !isGIF { + pixbuf, _ = l.GetPixbuf() + } else { + pixbuf, _ = l.GetAnimation() } - execIfCtx(ctx, func() { img.SetFromPixbuf(p) }) - } else { - p, err := l.GetAnimation() - if err != nil { - log.Error(errors.Wrap(err, "Failed to get animation")) - return - } - execIfCtx(ctx, func() { img.SetFromAnimation(p) }) + } + + switch pixbuf := pixbuf.(type) { + case *gdk.Pixbuf: + execIfCtx(ctx, func() { img.SetFromPixbuf(pixbuf) }) + case *gdk.PixbufAnimation: + execIfCtx(ctx, func() { img.SetFromAnimation(pixbuf) }) } } } func execIfCtx(ctx context.Context, fn func()) { - gts.ExecAsync(func() { + gts.ExecLater(func() { if ctx.Err() == nil { fn() } }) } -func downloadImage(ctx context.Context, dst io.WriteCloser, url string, p []imgutil.Processor, gif bool) { - // Close at the end when done. - defer dst.Close() - - r, err := get(ctx, url, true) - if err != nil { - log.Error(err) - return - } - defer r.Body.Close() +func downloadImage(src io.Reader, dst io.Writer, p []imgutil.Processor, isGIF bool) error { + var err error // If we have processors, then write directly in there. if len(p) > 0 { - if !gif { - err = imgutil.ProcessStream(dst, r.Body, p) + if !isGIF { + err = imgutil.ProcessStream(dst, src, p) } else { - err = imgutil.ProcessAnimationStream(dst, r.Body, p) + err = imgutil.ProcessAnimationStream(dst, src, p) } } else { // Else, directly copy the body over. - _, err = io.Copy(dst, r.Body) + _, err = io.Copy(dst, src) } if err != nil { - log.Error(errors.Wrap(err, "Error processing image")) - return + return errors.Wrap(err, "failed to process image") } + + return nil } diff --git a/internal/gts/throttler/throttler.go b/internal/gts/throttler/throttler.go index 4ecdc50..68b3ac2 100644 --- a/internal/gts/throttler/throttler.go +++ b/internal/gts/throttler/throttler.go @@ -16,7 +16,7 @@ type State struct { } type Connector interface { - Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error) + Connect(string, interface{}) glib.SignalHandle } func Bind(app *gtk.Application) *State { @@ -34,8 +34,8 @@ func Bind(app *gtk.Application) *State { } func (s *State) Connect(c Connector) { - c.Connect("focus-out-event", s.Start) - c.Connect("focus-in-event", s.Stop) + c.Connect("focus-out-event", func(interface{}) { s.Start() }) + c.Connect("focus-in-event", func(interface{}) { s.Stop() }) } func (s *State) Start() { diff --git a/internal/ui/config/preferences/preferences.go b/internal/ui/config/preferences/preferences.go index a042937..4182e5e 100644 --- a/internal/ui/config/preferences/preferences.go +++ b/internal/ui/config/preferences/preferences.go @@ -81,7 +81,7 @@ func NewPreferenceDialog() *Dialog { func SpawnPreferenceDialog() { p := NewPreferenceDialog() - p.Connect("destroy", func() { + p.Connect("destroy", func(interface{}) { // On close, save the settings. if err := config.Save(); err != nil { log.Error(errors.Wrap(err, "Failed to save settings")) diff --git a/internal/ui/config/widgets.go b/internal/ui/config/widgets.go index b719c3b..85bbd06 100644 --- a/internal/ui/config/widgets.go +++ b/internal/ui/config/widgets.go @@ -36,7 +36,7 @@ func (c *_combo) Construct() gtk.IWidget { combo.Append(opt, opt) } - combo.Connect("changed", func() { c.set(combo.GetActive()) }) + combo.Connect("changed", func(combo *gtk.ComboBoxText) { c.set(combo.GetActive()) }) combo.SetActive(*c.selected) combo.SetHAlign(gtk.ALIGN_END) combo.Show() @@ -76,7 +76,7 @@ func (s *_switch) set(v bool) { func (s *_switch) Construct() gtk.IWidget { sw, _ := gtk.SwitchNew() sw.SetActive(*s.value) - sw.Connect("notify::active", func() { s.set(sw.GetActive()) }) + sw.Connect("notify::active", func(sw *gtk.Switch) { s.set(sw.GetActive()) }) sw.SetHAlign(gtk.ALIGN_END) sw.Show() @@ -118,7 +118,7 @@ func (e *_inputentry) Construct() gtk.IWidget { entry.SetHExpand(true) entry.SetText(*e.value) - entry.Connect("changed", func() { + entry.Connect("changed", func(entry *gtk.Entry) { v, err := entry.GetText() if err != nil { return diff --git a/internal/ui/dialog/dialog.go b/internal/ui/dialog/dialog.go index 9344d94..dac407f 100644 --- a/internal/ui/dialog/dialog.go +++ b/internal/ui/dialog/dialog.go @@ -53,8 +53,8 @@ func NewModal(body gtk.IWidget, title, button string, clicked func(m *Modal)) *M header, } - cancel.Connect("clicked", dialog.Destroy) - action.Connect("clicked", func() { clicked(modald) }) + cancel.Connect("clicked", func(interface{}) { dialog.Destroy() }) + action.Connect("clicked", func(interface{}) { clicked(modald) }) return modald } @@ -78,7 +78,7 @@ func newCSD(body, header gtk.IWidget) *gtk.Dialog { dialog.Add(body) if oldh, _ := dialog.GetHeaderBar(); oldh != nil { - dialog.Remove(oldh) + oldh.ToWidget().Destroy() } dialog.SetTitlebar(header) diff --git a/internal/ui/messages/container/cozy/message_full.go b/internal/ui/messages/container/cozy/message_full.go index 4ee4d50..f5ed3a5 100644 --- a/internal/ui/messages/container/cozy/message_full.go +++ b/internal/ui/messages/container/cozy/message_full.go @@ -66,9 +66,9 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage { avatar := NewAvatar() avatar.SetMarginTop(TopFullMargin) avatar.SetMarginStart(container.ColumnSpacing * 2) - avatar.Connect("clicked", func() { + avatar.Connect("clicked", func(w gtk.IWidget) { if output := gc.Username.Output(); len(output.Mentions) > 0 { - labeluri.PopoverMentioner(avatar, output.Input, output.Mentions[0]) + labeluri.PopoverMentioner(w, output.Input, output.Mentions[0]) } }) // We don't call avatar.Show(). That's called in Attach. diff --git a/internal/ui/messages/header.go b/internal/ui/messages/header.go index 6fb3c97..29a30b7 100644 --- a/internal/ui/messages/header.go +++ b/internal/ui/messages/header.go @@ -95,12 +95,12 @@ func (h *Header) Reset() { } func (h *Header) OnBackPressed(fn func()) { - h.BackButton.Connect("clicked", fn) + h.BackButton.Connect("clicked", func(*gtk.Button) { fn() }) } func (h *Header) OnShowMembersToggle(fn func(show bool)) { - h.ShowMembers.Connect("toggled", func() { - fn(h.ShowMembers.GetActive()) + h.ShowMembers.Connect("toggled", func(showMembers *gtk.ToggleButton) { + fn(showMembers.GetActive()) }) } diff --git a/internal/ui/messages/input/attachment/attachment.go b/internal/ui/messages/input/attachment/attachment.go index 674bc27..e92f821 100644 --- a/internal/ui/messages/input/attachment/attachment.go +++ b/internal/ui/messages/input/attachment/attachment.go @@ -1,32 +1,23 @@ package attachment import ( - "bytes" "fmt" - "image" - "image/png" "io" - "io/ioutil" "mime" "os" "path/filepath" "strings" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/log" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage" - "github.com/disintegration/imaging" + "github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gtk" "github.com/pkg/errors" ) -var pngEncoder = png.Encoder{ - CompressionLevel: png.BestCompression, -} - const ( ThumbSize = 72 IconSize = 56 @@ -37,7 +28,7 @@ const ( type File struct { Prog *Progress Name string - Size int64 + Size int64 // -1 = stream } // NewFile creates a new attachment file with a progress state. @@ -70,7 +61,7 @@ type Container struct { // states files []File - items map[string]gtk.IWidget + items map[string]primitives.WidgetDestroyer } var attachmentsCSS = primitives.PrepareCSS(` @@ -120,7 +111,7 @@ func New() *Container { Revealer: rev, Scroll: scr, Box: box, - items: map[string]gtk.IWidget{}, + items: map[string]primitives.WidgetDestroyer{}, } } @@ -155,11 +146,11 @@ func (c *Container) Reset() { // Clear all items. for _, item := range c.items { - c.Box.Remove(item) + item.Destroy() } // Reset the map. - c.items = map[string]gtk.IWidget{} + c.items = map[string]primitives.WidgetDestroyer{} // Hide the window. c.SetRevealChild(false) @@ -187,61 +178,31 @@ func (c *Container) AddFile(path string) error { func() (io.ReadCloser, error) { return os.Open(path) }, ) + scale := c.GetScaleFactor() + // Maybe try making a preview. A nil image is fine, so we can skip the error // check. // TODO: add a filesize check - i, _ := imaging.Open(path, imaging.AutoOrientation(true)) - c.addPreview(filename, i) - + pixbuf, _ := gdk.PixbufNewFromFileAtScale(path, ThumbSize*scale, ThumbSize*scale, true) + c.addPreview(filename, thumbnailPixbuf(pixbuf, scale)) return nil } // AddPixbuf is used for adding pixbufs from the clipboard. -func (c *Container) AddPixbuf(pb *gdk.Pixbuf) error { - // Pixbuf's colorspace is only RGB. This is indicated with - // GDK_COLORSPACE_RGB. - if pb.GetColorspace() != gdk.COLORSPACE_RGB { - return errors.New("Pixbuf has unsupported colorspace") - } - - // Assert that the pixbuf has alpha, as we're using RGBA. - if !pb.GetHasAlpha() { - return errors.New("Pixbuf has no alpha channel") - } - - // Assert that there are 4 channels: red, green, blue and alpha. - if pb.GetNChannels() != 4 { - return errors.New("Pixbuf has unexpected channel count") - } - - // Assert that there are 8 bits in a channel/sample. - if pb.GetBitsPerSample() != 8 { - return errors.New("Pixbuf has unexpected bits per sample") - } - - var img = &image.NRGBA{ - Pix: pb.GetPixels(), - Stride: pb.GetRowstride(), - Rect: image.Rect(0, 0, pb.GetWidth(), pb.GetHeight()), - } - - // Store the image in memory. - var buf bytes.Buffer - - if err := pngEncoder.Encode(&buf, img); err != nil { - return errors.Wrap(err, "Failed to encode PNG") - } - +func (c *Container) AddPixbuf(pb *gdk.Pixbuf) { var filename = c.append( - fmt.Sprintf("clipboard_%d.png", len(c.files)+1), int64(buf.Len()), + fmt.Sprintf("clipboard_%d.png", len(c.files)+1), -1, func() (io.ReadCloser, error) { - return ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil + r, w := io.Pipe() + go func() { w.CloseWithError(pb.WritePNG(w, 9)) }() + return r, nil }, ) - c.addPreview(filename, img) + scale := c.GetScaleFactor() - return nil + c.addPreview(filename, thumbnailPixbuf(pb, scale)) + return } // -- internal methods -- @@ -273,7 +234,7 @@ func (c *Container) remove(name string) { } if w, ok := c.items[name]; ok { - c.Box.Remove(w) + w.Destroy() delete(c.items, name) } @@ -309,7 +270,7 @@ var deleteAttBtnCSS = primitives.PrepareCSS(` } `) -func (c *Container) addPreview(name string, src image.Image) { +func (c *Container) addPreview(name string, thumbnail *cairo.Surface) { // Make a fallback image first. gimg, _ := roundimage.NewImage(4) // border-radius: 4px primitives.SetImageIcon(gimg.Image, iconFromName(name), IconSize) @@ -322,19 +283,8 @@ func (c *Container) addPreview(name string, src image.Image) { primitives.AttachCSS(gimg, previewCSS) // Determine if we could generate an image preview. - if src != nil { - // Get the minimum dimension. - var w, h = minsize(src.Bounds().Dx(), src.Bounds().Dy(), ThumbSize) - - var img *image.NRGBA - // Downscale the image. - img = imaging.Resize(src, w, h, imaging.Lanczos) - - // Crop to a square. - img = imaging.CropCenter(img, ThumbSize, ThumbSize) - - // Copy the image to a pixbuf. - gimg.SetFromPixbuf(gts.RenderPixbuf(img)) + if thumbnail != nil { + gimg.SetFromSurface(thumbnail) } // BLOAT!!! Make an overlay of an event box that, when hovered, will show @@ -343,7 +293,7 @@ func (c *Container) addPreview(name string, src image.Image) { del.SetVAlign(gtk.ALIGN_CENTER) del.SetHAlign(gtk.ALIGN_CENTER) del.SetTooltipText("Remove " + name) - del.Connect("clicked", func() { c.remove(name) }) + del.Connect("clicked", func(del *gtk.Button) { c.remove(name) }) del.Show() primitives.AddClass(del, "delete-attachment") primitives.AttachCSS(del, deleteAttBtnCSS) @@ -358,6 +308,58 @@ func (c *Container) addPreview(name string, src image.Image) { c.Box.PackStart(ovl, false, false, 0) } +func thumbnailPixbuf(pixbuf *gdk.Pixbuf, scale int) *cairo.Surface { + if pixbuf == nil { + return nil + } + + var ( + originalWidth = pixbuf.GetWidth() + originalHeight = pixbuf.GetHeight() + + scaledThumbSize = ThumbSize * scale + scaledWidth, scaledHeight = minsize(originalWidth, originalHeight, scaledThumbSize) + + // offset of src on thumbnail; one of those will be 0 + offsetX = float64(scaledThumbSize-scaledWidth) / 2 + offsetY = float64(scaledThumbSize-scaledHeight) / 2 + ) + + thumbnail, err := gdk.PixbufNew( + pixbuf.GetColorspace(), + true, 8, // always have alpha, 8bpc + scaledThumbSize, scaledThumbSize, + ) + + if err != nil { + panic("failed to allocate upload thumbnail pixbuf: " + err.Error()) + } + + // Fill with transparent pixels. + thumbnail.Fill(0x0) + + pixbuf.Scale( + thumbnail, + int(offsetX), int(offsetY), + // size of src on thumbnail + scaledWidth, scaledHeight, + // no offset on source image + offsetX, offsetY, + // scale ratio for both sides + float64(scaledWidth)/float64(originalWidth), + float64(scaledHeight)/float64(originalHeight), + // expensive rescale algorithm + gdk.INTERP_HYPER, + ) + + surface, err := gdk.CairoSurfaceCreateFromPixbuf(thumbnail, scale, nil) + if err != nil { + panic("failed to create thumbnail cairo surface: " + err.Error()) + } + + return surface +} + func iconFromName(filename string) string { switch t := mime.TypeByExtension(filepath.Ext(filename)); { case strings.HasPrefix(t, "image"): @@ -376,8 +378,9 @@ func iconFromName(filename string) string { } } +// minsize returns the scaled size so that the largest edge is maxsz. func minsize(w, h, maxsz int) (int, int) { - if w < h { + if w > h { // return the scaled width as max // h*max/w is the same as h/w*max but with more accuracy return maxsz, h * maxsz / w @@ -385,3 +388,17 @@ func minsize(w, h, maxsz int) (int, int) { return w * maxsz / h, maxsz } + +func min(w, h int) int { + if w > h { + return h + } + return w +} + +func max(w, h int) int { + if w > h { + return w + } + return h +} diff --git a/internal/ui/messages/input/attachment/progress.go b/internal/ui/messages/input/attachment/progress.go index d125f63..d20d12c 100644 --- a/internal/ui/messages/input/attachment/progress.go +++ b/internal/ui/messages/input/attachment/progress.go @@ -6,6 +6,7 @@ import ( "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" + "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/pango" ) @@ -47,7 +48,12 @@ func NewProgressBar(file File) *ProgressBar { bar.SetVAlign(gtk.ALIGN_CENTER) bar.Show() - name, _ := gtk.LabelNew(file.Name) + var label = file.Name + if file.Size > 0 { + label += " - " + glib.FormatSize(uint64(file.Size)) + } + + name, _ := gtk.LabelNew(label) name.SetMaxWidthChars(45) name.SetSingleLineMode(true) name.SetEllipsize(pango.ELLIPSIZE_MIDDLE) diff --git a/internal/ui/messages/input/input.go b/internal/ui/messages/input/input.go index 04db742..4480276 100644 --- a/internal/ui/messages/input/input.go +++ b/internal/ui/messages/input/input.go @@ -196,16 +196,23 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field { // Bind text events. text.Connect("key-press-event", field.keyDown) // Bind the send button. - field.send.Connect("clicked", field.sendInput) + field.send.Connect("clicked", func(*gtk.Button) { field.sendInput() }) // Bind the attach button. - field.attach.Connect("clicked", func() { gts.SpawnUploader("", field.Attachments.AddFiles) }) + field.attach.Connect("clicked", func(attach *gtk.Button) { + gts.SpawnUploader("", field.Attachments.AddFiles) + }) + + // allocatedWidthGetter is used below. + type allocatedWidthGetter interface { + GetAllocatedWidth() int + } // Connect to the field's revealer. On resize, we want the attachments // carousel to have the same padding too. - field.Username.Connect("size-allocate", func(w gtk.IWidget) { + field.Username.Connect("size-allocate", func(w allocatedWidthGetter) { // Calculate the left width: from the left of the message box to the // right of the attach button, covering the username container. - var leftWidth = 5 + field.attach.GetAllocatedWidth() + w.ToWidget().GetAllocatedWidth() + var leftWidth = 5 + field.attach.GetAllocatedWidth() + w.GetAllocatedWidth() // Set the autocompleter's left margin to be the same. field.Attachments.SetMarginStart(leftWidth) }) diff --git a/internal/ui/messages/input/keydown.go b/internal/ui/messages/input/keydown.go index 08fee65..db8ecf7 100644 --- a/internal/ui/messages/input/keydown.go +++ b/internal/ui/messages/input/keydown.go @@ -84,23 +84,23 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool { return false } + // TODO: make this asynchronous. + // Is there an image in the clipboard? if !gts.Clipboard.WaitIsImageAvailable() { - // No. return false } - // Yes. - p, err := gts.Clipboard.WaitForImage() - if err != nil { - log.Error(errors.Wrap(err, "Failed to get image from clipboard")) - return true // interrupt as technically valid - } + gts.Async(func() (func(), error) { + p, err := gts.Clipboard.WaitForImage() + if err != nil { + return nil, errors.Wrap(err, "Failed to get image from clipboard") + } - if err := f.Attachments.AddPixbuf(p); err != nil { - log.Error(errors.Wrap(err, "Failed to add image to attachment list")) - return true - } + return func() { f.Attachments.AddPixbuf(p) }, nil + }) + + return true } // If the server supports typing indication, then announce that we are diff --git a/internal/ui/messages/memberlist/memberlist.go b/internal/ui/messages/memberlist/memberlist.go index 86f5156..cf4d917 100644 --- a/internal/ui/messages/memberlist/memberlist.go +++ b/internal/ui/messages/memberlist/memberlist.go @@ -87,7 +87,7 @@ func (c *Container) Reset() { c.Revealer.SetRevealChild(false) for _, section := range c.Sections { - c.Main.Remove(section) + section.Destroy() } c.Sections = map[string]*Section{} @@ -276,7 +276,7 @@ func (s *Section) RemoveMember(id string) { } } -func listSortNameAsc(r1, r2 *gtk.ListBoxRow, _ ...interface{}) int { +func listSortNameAsc(r1, r2 *gtk.ListBoxRow) int { n1, _ := r1.GetName() n2, _ := r2.GetName() @@ -400,7 +400,7 @@ func (m *Member) Popup(evq EventQueuer) { // Unbounded concurrency is kind of bad. We should deal with // this in the future. evq.Activate() - p.Connect("closed", evq.Deactivate) + p.Connect("closed", func(interface{}) { evq.Deactivate() }) p.SetPosition(gtk.POS_LEFT) p.Popup() diff --git a/internal/ui/messages/typing/typing.go b/internal/ui/messages/typing/typing.go index 75d1f8f..509f0ad 100644 --- a/internal/ui/messages/typing/typing.go +++ b/internal/ui/messages/typing/typing.go @@ -58,7 +58,7 @@ func New() *Container { }) // On label destroy, stop the state loop as well. - l.Connect("destroy", state.stopper) + l.Connect("destroy", func(interface{}) { state.stopper() }) return &Container{ Revealer: r, diff --git a/internal/ui/messages/view.go b/internal/ui/messages/view.go index d7d1b0b..4ed7f21 100644 --- a/internal/ui/messages/view.go +++ b/internal/ui/messages/view.go @@ -155,7 +155,8 @@ func NewView(c Controller) *View { drag.BindFileDest(view.LeftBox, view.InputView.Attachments.AddFiles) // placeholder logo - logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256Variant2(128)) + logo, _ := gtk.ImageNew() + logo.SetFromSurface(icons.Logo256Variant2(128, logo.GetScaleFactor())) logo.Show() view.FaceView = sadface.New(view.Leaflet, logo) @@ -201,6 +202,7 @@ func (v *View) createMessageContainer() { // Remove the old message container. if v.Container != nil { + v.Container.Reset() v.MsgBox.Remove(v.Container) } @@ -221,13 +223,19 @@ func (v *View) createMessageContainer() { func (v *View) Bottomed() bool { return v.Scroller.Bottomed } +// Reset resets the message view. func (v *View) Reset() { + v.FaceView.Reset() // Switch back to the main screen. + v.reset() +} + +// reset resets the message view, but does not change visible containers. +func (v *View) reset() { v.Header.Reset() // Reset the header. v.state.Reset() // Reset the state variables. v.Typing.Reset() // Reset the typing state. v.InputView.Reset() // Reset the input. v.MemberList.Reset() // Reset the member list. - v.FaceView.Reset() // Switch back to the main screen. // Bring the leaflet view back to the message. v.Leaflet.SetVisibleChild(v.LeftBox) @@ -282,16 +290,8 @@ func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc travers v.FaceView.SetLoading() v.ctrl.OnMessageBusy() - // We can be dumb. Reset afterwards so the animation goes smoother. - gts.DoAfterMs( - v.FaceView.GetTransitionDuration(), - func() { v.joinServer(session, server, bc) }, - ) -} - -func (v *View) joinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) { // Reset before setting. - v.Reset() + v.reset() // Get the messenger once. var messenger = server.AsMessenger() diff --git a/internal/ui/primitives/actions/menubutton.go b/internal/ui/primitives/actions/menubutton.go index 0622093..64f476d 100644 --- a/internal/ui/primitives/actions/menubutton.go +++ b/internal/ui/primitives/actions/menubutton.go @@ -46,7 +46,7 @@ func (m *MenuButton) Bind(menu *Menu) { // menu items. m.SetSensitive(model.GetNItems() > 0) // Subscribe the button to menu update events. - m.lastsig, _ = model.Connect("items-changed", func() { + m.lastsig = model.Connect("items-changed", func(model *glib.MenuModel) { m.SetSensitive(model.GetNItems() > 0) }) } else { diff --git a/internal/ui/primitives/buttonoverlay/buttonoverlay.go b/internal/ui/primitives/buttonoverlay/buttonoverlay.go index bbca90c..21f5636 100644 --- a/internal/ui/primitives/buttonoverlay/buttonoverlay.go +++ b/internal/ui/primitives/buttonoverlay/buttonoverlay.go @@ -46,10 +46,6 @@ func Take(b, smallbutton Button, size int) { childv, _ := b.GetChild() widget := childv.ToWidget() - // As GetChild doesn't reference, we'll want our own reference. - widget.Ref() - defer widget.Unref() - // This will unreference. b.Remove(widget) // Wrap will reference. diff --git a/internal/ui/primitives/completion/completer.go b/internal/ui/primitives/completion/completer.go index 181d429..01c49e1 100644 --- a/internal/ui/primitives/completion/completer.go +++ b/internal/ui/primitives/completion/completer.go @@ -6,6 +6,7 @@ import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/gts/httputil" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput" "github.com/diamondburned/cchat-gtk/internal/ui/rich" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup" @@ -68,9 +69,9 @@ func NewCompleter(input *gtk.TextView) *Completer { } // This one is for buffer modification. - ibuf.Connect("end-user-action", c.onChange) + ibuf.Connect("end-user-action", func(interface{}) { c.onChange() }) // This one is for when the cursor moves. - input.Connect("move-cursor", c.onChange) + input.Connect("move-cursor", func(interface{}) { c.onChange() }) l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) { SwapWord(ibuf, c.entries[r.GetIndex()].Raw, c.cursor) @@ -115,9 +116,7 @@ func (c *Completer) Clear() { } children.Foreach(func(i interface{}) { - w := i.(gtk.IWidget).ToWidget() - c.List.Remove(w) - w.Destroy() + i.(primitives.WidgetDestroyer).Destroy() }) } diff --git a/internal/ui/primitives/drag/drag.go b/internal/ui/primitives/drag/drag.go index 5a3603f..c8808b3 100644 --- a/internal/ui/primitives/drag/drag.go +++ b/internal/ui/primitives/drag/drag.go @@ -104,21 +104,21 @@ func BindDraggable(dg MainDraggable, icon string, fn Swapper, draggers ...Dragga dragger.DragSourceSet(gdk.BUTTON1_MASK, dragEntries, gdk.ACTION_MOVE) dragger.Connect("drag-data-get", - func(_ gtk.IWidget, ctx *gdk.DragContext, data *gtk.SelectionData) { + func(_ interface{}, ctx *gdk.DragContext, data *gtk.SelectionData) { // Set the index-in-bytes. data.SetData(dragAtom, []byte(dg.ID())) }, ) dragger.Connect("drag-begin", - func(_ gtk.IWidget, ctx *gdk.DragContext) { + func(_ interface{}, ctx *gdk.DragContext) { gtk.DragSetIconName(ctx, icon, 0, 0) dg.SetSensitive(false) }, ) dragger.Connect("drag-end", - func() { + func(interface{}) { dg.SetSensitive(true) }, ) diff --git a/internal/ui/primitives/menu/menu.go b/internal/ui/primitives/menu/menu.go index 3f608fa..5231ee4 100644 --- a/internal/ui/primitives/menu/menu.go +++ b/internal/ui/primitives/menu/menu.go @@ -117,7 +117,7 @@ func SimpleItem(name string, fn func()) Item { func (item Item) ToMenuItem() *gtk.MenuItem { mb, _ := gtk.MenuItemNewWithLabel(item.Name) - mb.Connect("activate", item.Func) + mb.Connect("activate", func(interface{}) { item.Func() }) mb.Show() if item.Extra != nil { @@ -129,7 +129,7 @@ func (item Item) ToMenuItem() *gtk.MenuItem { func (item Item) ToToolButton() *gtk.ToolButton { tb, _ := gtk.ToolButtonNew(nil, item.Name) - tb.Connect("clicked", item.Func) + tb.Connect("clicked", func(interface{}) { item.Func() }) tb.Show() return tb diff --git a/internal/ui/primitives/primitives.go b/internal/ui/primitives/primitives.go index e89665b..84b51d0 100644 --- a/internal/ui/primitives/primitives.go +++ b/internal/ui/primitives/primitives.go @@ -13,6 +13,11 @@ import ( "github.com/pkg/errors" ) +type WidgetDestroyer interface { + gtk.IWidget + Destroy() +} + type Container interface { Remove(gtk.IWidget) GetChildren() *glib.List @@ -21,8 +26,12 @@ type Container interface { var _ Container = (*gtk.Container)(nil) func RemoveChildren(w Container) { + type destroyer interface { + Destroy() + } + w.GetChildren().Foreach(func(child interface{}) { - w.Remove(child.(gtk.IWidget)) + child.(destroyer).Destroy() }) } @@ -166,18 +175,18 @@ func MenuItem(label string, fn interface{}) *gtk.MenuItem { } type Connector interface { - Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error) - ConnectAfter(string, interface{}, ...interface{}) (glib.SignalHandle, error) + Connect(string, interface{}) glib.SignalHandle + ConnectAfter(string, interface{}) glib.SignalHandle } func HandleDestroyCtx(ctx context.Context, connector Connector) context.Context { ctx, cancel := context.WithCancel(ctx) - connector.Connect("destroy", cancel) + connector.Connect("destroy", func(c Connector) { cancel() }) return ctx } func BindMenu(connector Connector, menu *gtk.Menu) { - connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) { + connector.Connect("button-press-event", func(c Connector, ev *gdk.Event) { if gts.EventIsRightClick(ev) { menu.PopupAtPointer(ev) } @@ -185,7 +194,7 @@ func BindMenu(connector Connector, menu *gtk.Menu) { } func BindDynamicMenu(connector Connector, constr func(menu *gtk.Menu)) { - connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) { + connector.Connect("button-press-event", func(c Connector, ev *gdk.Event) { if gts.EventIsRightClick(ev) { menu, _ := gtk.MenuNew() constr(menu) @@ -287,7 +296,7 @@ func InlineCSS(ctx StyleContexter, css string) { // LeafletOnFold binds a callback to a leaflet that would be called when the // leaflet's folded state changes. func LeafletOnFold(leaflet *handy.Leaflet, foldedFn func(folded bool)) { - leaflet.ConnectAfter("notify::folded", func() { + leaflet.ConnectAfter("notify::folded", func(leaflet *handy.Leaflet) { foldedFn(leaflet.GetFolded()) }) } diff --git a/internal/ui/primitives/roundimage/avatar.go b/internal/ui/primitives/roundimage/avatar.go index 36d685e..b8c3196 100644 --- a/internal/ui/primitives/roundimage/avatar.go +++ b/internal/ui/primitives/roundimage/avatar.go @@ -85,13 +85,13 @@ func (a *Avatar) loadFunc(size int) *gdk.Pixbuf { return nil } - // Temporarily resize for now. - p, err := a.pixbuf.ScaleSimple(size, size, gdk.INTERP_HYPER) - if err != nil { - p = a.pixbuf - } + // // Temporarily resize for now. + // p, err := a.pixbuf.ScaleSimple(size, size, gdk.INTERP_HYPER) + // if err != nil { + // p = a.pixbuf + // } - return p + return a.pixbuf } // SetRadius is a no-op. diff --git a/internal/ui/primitives/roundimage/roundimage.go b/internal/ui/primitives/roundimage/roundimage.go index 8cc4b86..cac0188 100644 --- a/internal/ui/primitives/roundimage/roundimage.go +++ b/internal/ui/primitives/roundimage/roundimage.go @@ -63,7 +63,7 @@ func NewImage(radius float64) (*Image, error) { // Backup plan if Cairo's Surface is weird. // var width, height int - // i.Connect("size-allocate", func() { + // i.Connect("size-allocate", func(i *gtk.Image) { // w := i.GetAllocatedWidth() // h := i.GetAllocatedHeight() @@ -93,7 +93,7 @@ func (i *Image) SetRadius(r float64) { i.Radius = r } -func (i *Image) drawer(widget gtk.IWidget, cc *cairo.Context) bool { +func (i *Image) drawer(_ interface{}, cc *cairo.Context) bool { var w = float64(i.GetAllocatedWidth()) var h = float64(i.GetAllocatedHeight()) diff --git a/internal/ui/primitives/roundimage/static.go b/internal/ui/primitives/roundimage/static.go index cefa9ce..2ecfc98 100644 --- a/internal/ui/primitives/roundimage/static.go +++ b/internal/ui/primitives/roundimage/static.go @@ -5,6 +5,7 @@ import ( "github.com/diamondburned/cchat-gtk/internal/gts/httputil" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" + "github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/gdk" ) @@ -36,13 +37,13 @@ func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage, } func (s *StaticImage) ConnectHandlers(connector primitives.Connector) { - connector.Connect("enter-notify-event", func() { + connector.Connect("enter-notify-event", func(interface{}) { if s.animation != nil && !s.animating { s.animating = true s.Image.SetFromAnimation(s.animation) } }) - connector.Connect("leave-notify-event", func() { + connector.Connect("leave-notify-event", func(interface{}) { if s.animation != nil && s.animating { s.animating = false s.Image.SetFromPixbuf(s.animation.GetStaticImage()) @@ -60,6 +61,11 @@ func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) { s.Image.SetFromPixbuf(pb) } +func (s *StaticImage) SetFromSurface(sf *cairo.Surface) { + s.animation = nil + s.Image.SetFromSurface(sf) +} + func (s *StaticImage) SetFromAnimation(anim *gdk.PixbufAnimation) { s.animation = anim s.Image.SetFromPixbuf(anim.GetStaticImage()) diff --git a/internal/ui/rich/image.go b/internal/ui/rich/image.go index 6efab0c..24af0c5 100644 --- a/internal/ui/rich/image.go +++ b/internal/ui/rich/image.go @@ -132,7 +132,7 @@ func (i *Icon) AsyncSetIconer(iconer cchat.Iconer, errwrap string) { return nil, errors.Wrap(err, "failed to load iconer") } - return func() { i.Connect("destroy", f) }, nil + return func() { i.Connect("destroy", func(interface{}) { f() }) }, nil }) } diff --git a/internal/ui/rich/label.go b/internal/ui/rich/label.go index 0836b4c..8c17022 100644 --- a/internal/ui/rich/label.go +++ b/internal/ui/rich/label.go @@ -79,7 +79,7 @@ func (l *Label) AsyncSetLabel(fn LabelerFn, info string) { return nil, errors.Wrap(err, "failed to load iconer") } - return func() { l.Connect("destroy", f) }, nil + return func() { l.Connect("destroy", func(interface{}) { f() }) }, nil }) } diff --git a/internal/ui/rich/labeluri/labeluri.go b/internal/ui/rich/labeluri/labeluri.go index e8fd83c..be885eb 100644 --- a/internal/ui/rich/labeluri/labeluri.go +++ b/internal/ui/rich/labeluri/labeluri.go @@ -179,7 +179,6 @@ func NewPopoverMentioner(rel gtk.IWidget, input string, segment text.Segment) *g p, _ := gtk.PopoverNew(rel) p.Add(box) p.SetSizeRequest(PopoverWidth, -1) - p.Connect("destroy", box.Destroy) return p } @@ -216,7 +215,7 @@ func popoverImg(url string, round bool) gtk.IWidget { btn.SetHAlign(gtk.ALIGN_CENTER) btn.SetRelief(gtk.RELIEF_NONE) - btn.Connect("clicked", func() { PromptOpen(url) }) + btn.Connect("clicked", func(*gtk.Button) { PromptOpen(url) }) btn.Show() return btn @@ -236,7 +235,7 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle) // message, but we're also keeping alive the widget. var x, y float64 - connector.Connect("motion-notify-event", func(w gtk.IWidget, ev *gdk.Event) { + connector.Connect("motion-notify-event", func(_ interface{}, ev *gdk.Event) { x, y = gdk.EventMotionNewFromEvent(ev).MotionVal() }) @@ -274,12 +273,11 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle) btn, _ := gtk.ButtonNew() btn.Add(img) btn.SetRelief(gtk.RELIEF_NONE) - btn.Connect("clicked", func() { PromptOpen(uri) }) + btn.Connect("clicked", func(*gtk.Button) { PromptOpen(uri) }) btn.Show() p, _ := gtk.PopoverNew(c) p.SetPointingTo(r) - p.Connect("closed", img.Destroy) // on close, destroy image p.Add(btn) p.Popup() diff --git a/internal/ui/rich/parser/attrmap/attrmap.go b/internal/ui/rich/parser/attrmap/attrmap.go index 25e2263..70208b7 100644 --- a/internal/ui/rich/parser/attrmap/attrmap.go +++ b/internal/ui/rich/parser/attrmap/attrmap.go @@ -40,14 +40,14 @@ func (a *AppendMap) Anchor(start, end int, href string) { // AnchorNU makes a new tag without underlines and colors. func (a *AppendMap) AnchorNU(start, end int, href string) { - a.Openf(start, ``, html.EscapeString(href)) + a.Openf(start, ``) a.Close(end, "") // a.Anchor(start, end, href) a.Span(start, end, `underline="none"`) } func (a *AppendMap) Span(start, end int, attrs ...string) { - a.Openf(start, "", strings.Join(attrs, " ")) + a.Open(start, "") a.Close(end, "") } diff --git a/internal/ui/rich/parser/markup/markup.go b/internal/ui/rich/parser/markup/markup.go index 5a7baef..0fc6bd0 100644 --- a/internal/ui/rich/parser/markup/markup.go +++ b/internal/ui/rich/parser/markup/markup.go @@ -6,6 +6,7 @@ import ( "html" "net/url" "sort" + "strconv" "strings" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/attrmap" @@ -205,29 +206,37 @@ func colorAttrs(c uint32, bg bool) []string { rgb, a := splitRGBA(c) // Render the hex representation beforehand. - hex := fmt.Sprintf("#%06X", rgb) + hex := "#" + strconv.FormatUint(uint64(rgb), 16) attrs := make([]string, 1, 4) - attrs[0] = fmt.Sprintf(`color="%s"`, hex) + attrs[0] = wrapKeyValue("color", hex) // If we have an alpha that isn't solid (100%), then write it. if a < 0xFF { // Calculate alpha percentage. perc := a * 100 / 255 - attrs = append(attrs, fmt.Sprintf(`fgalpha="%d%%"`, perc)) + attrs = append(attrs, wrapKeyValue("fgalpha", strconv.Itoa(int(perc)))) } // Draw a faded background if we explicitly requested for one. if bg { // Calculate how faded the background should be for visual purposes. perc := a * 10 / 255 // always 10% or less. - attrs = append(attrs, fmt.Sprintf(`bgalpha="%d%%"`, perc)) - attrs = append(attrs, fmt.Sprintf(`bgcolor="%s"`, hex)) + attrs = append(attrs, wrapKeyValue("bgalpha", strconv.Itoa(int(perc)))) + attrs = append(attrs, wrapKeyValue("bgcolor", hex)) } return attrs } +func hexPad(c uint32) string { + hex := strconv.FormatUint(uint64(c), 16) + if len(hex) >= 6 { + return hex + } + return strings.Repeat("0", 6-len(hex)) + hex +} + const ( // string constant for formatting width and height in URL fragments f_FragmentSize = "w=%d;h=%d" @@ -296,6 +305,17 @@ func span(key, value string) string { return "" } +func wrapKeyValue(key, value string) string { + buf := strings.Builder{} + buf.Grow(len(key) + len(value) + 3) + buf.WriteString(key) + buf.WriteByte('=') + buf.WriteByte('"') + buf.WriteString(value) + buf.WriteByte('"') + return buf.String() +} + func markupAttr(attr text.Attribute) string { // meme fast path if attr == 0 { diff --git a/internal/ui/service/auth/auth.go b/internal/ui/service/auth/auth.go index 472c428..db355be 100644 --- a/internal/ui/service/auth/auth.go +++ b/internal/ui/service/auth/auth.go @@ -65,7 +65,7 @@ func NewDialog(name text.Rich, authers []cchat.Authenticator, auth func(cchat.Se d.back, _ = gtk.ButtonNewFromIconName("go-previous-symbolic", gtk.ICON_SIZE_BUTTON) d.back.Show() - d.back.Connect("clicked", func() { + d.back.Connect("clicked", func(back *gtk.Button) { // If check just in case. if d.stageList != nil { d.leaflet.SetVisibleChild(d.stageList) diff --git a/internal/ui/service/config/widgets.go b/internal/ui/service/config/widgets.go index 19af2e4..18a298c 100644 --- a/internal/ui/service/config/widgets.go +++ b/internal/ui/service/config/widgets.go @@ -110,7 +110,7 @@ func newEntry(k string, conf map[string]string, change func()) *entry { e, _ := gtk.EntryNew() e.SetText(conf[k]) e.SetHExpand(true) - e.Connect("changed", func() { + e.Connect("changed", func(e *gtk.Entry) { conf[k], _ = e.GetText() change() }) diff --git a/internal/ui/service/header.go b/internal/ui/service/header.go index 80317c8..f7d7bde 100644 --- a/internal/ui/service/header.go +++ b/internal/ui/service/header.go @@ -18,7 +18,7 @@ type AppMenu struct { func NewAppMenu() *AppMenu { img, _ := gtk.ImageNew() - img.SetFromPixbuf(icons.Logo256(24)) + img.SetFromSurface(icons.Logo256(24, img.GetScaleFactor())) img.Show() appmenu, _ := gtk.MenuButtonNew() @@ -131,7 +131,7 @@ type sizeBinder interface { var _ sizeBinder = (*List)(nil) func (h *Header) AppMenuBindSize(c sizeBinder) { - c.Connect("size-allocate", func() { + c.Connect("size-allocate", func(c sizeBinder) { h.AppMenu.SetSizeRequest(c.GetAllocatedWidth(), -1) }) } diff --git a/internal/ui/service/list.go b/internal/ui/service/list.go index 30ef507..47e0f39 100644 --- a/internal/ui/service/list.go +++ b/internal/ui/service/list.go @@ -10,6 +10,7 @@ import ( ) type ViewController interface { + ClearMessenger(*session.Row) MessengerSelected(*session.Row, *server.ServerRow) SessionSelected(*Service, *session.Row) AuthenticateSession(*List, *Service) diff --git a/internal/ui/service/service.go b/internal/ui/service/service.go index 8b36b41..372d999 100644 --- a/internal/ui/service/service.go +++ b/internal/ui/service/service.go @@ -20,6 +20,8 @@ import ( const IconSize = 48 type ListController interface { + // ClearMessenger is called when a nil slice of servers is set. + ClearMessenger(*session.Row) // MessengerSelected is called when a server message row is clicked. MessengerSelected(*session.Row, *server.ServerRow) // SessionSelected tells the view to change the session view. @@ -35,6 +37,8 @@ type ListController interface { // Service holds everything that a single service has. type Service struct { + ListController + *gtk.Box Button *gtk.ToggleButton Icon *rich.Icon @@ -42,8 +46,7 @@ type Service struct { BodyRev *gtk.Revealer // revealed BodyList *session.List // not really supposed to be here - svclctrl ListController - service cchat.Service // state + service cchat.Service // state } var serviceCSS = primitives.PrepareClassCSS("service", ` @@ -81,8 +84,8 @@ var serviceIconCSS = primitives.PrepareClassCSS("service-icon", ` func NewService(svc cchat.Service, svclctrl ListController) *Service { service := &Service{ - service: svc, - svclctrl: svclctrl, + service: svc, + ListController: svclctrl, } service.BodyList = session.NewList(service) @@ -148,11 +151,11 @@ func (s *Service) GetRevealChild() bool { } func (s *Service) SessionSelected(srow *session.Row) { - s.svclctrl.SessionSelected(s, srow) + s.ListController.SessionSelected(s, srow) } func (s *Service) AuthenticateSession() { - s.svclctrl.AuthenticateSession(s) + s.ListController.AuthenticateSession(s) } func (s *Service) AddLoadingSession(id, name string) *session.Row { @@ -196,15 +199,11 @@ func (s *Service) OnSessionDisconnect(row *session.Row) { s.BodyList.UnselectAll() } - s.svclctrl.OnSessionDisconnect(s, row) -} - -func (s *Service) MessengerSelected(r *session.Row, sv *server.ServerRow) { - s.svclctrl.MessengerSelected(r, sv) + s.ListController.OnSessionDisconnect(s, row) } func (s *Service) RemoveSession(row *session.Row) { - s.svclctrl.OnSessionRemove(s, row) + s.ListController.OnSessionRemove(s, row) s.BodyList.RemoveSessionRow(row.ID()) s.SaveAllSessions() } diff --git a/internal/ui/service/session/server/button/button.go b/internal/ui/service/session/server/button/button.go index e575692..e598132 100644 --- a/internal/ui/service/session/server/button/button.go +++ b/internal/ui/service/session/server/button/button.go @@ -6,6 +6,7 @@ import ( "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/rich" "github.com/diamondburned/cchat/text" + "github.com/gotk3/gotk3/gtk" ) const UnreadColorDefs = ` @@ -61,10 +62,14 @@ func WrapToggleButtonImage(b *rich.ToggleButtonImage) *ToggleButtonImage { tb := &ToggleButtonImage{ ToggleButtonImage: b, - - clicked: func(bool) {}, + clicked: func(bool) {}, } - tb.Connect("clicked", func() { tb.clicked(tb.GetActive()) }) + + type activeGetter interface { + GetActive() bool + } + + tb.Connect("clicked", func(w *gtk.ToggleButton) { tb.clicked(w.GetActive()) }) serverButtonCSS(tb) return tb diff --git a/internal/ui/service/session/server/children.go b/internal/ui/service/session/server/children.go index 4cc4439..bfeec25 100644 --- a/internal/ui/service/session/server/children.go +++ b/internal/ui/service/session/server/children.go @@ -96,7 +96,7 @@ func (c *Children) Reset() { if row.IsHollow() { continue } - c.Box.Remove(row) + row.Destroy() } } @@ -141,38 +141,40 @@ func (c *Children) setNotLoading() { // Do we have the spinning circle button? If yes, remove it. if c.load != nil { // Stop the loading mode. The reset function should do everything for us. - c.Box.Remove(c.load) + c.load.Destroy() c.load = nil } } // SetServers is reserved for cchat.ServersContainer. func (c *Children) SetServers(servers []cchat.Server) { - gts.ExecAsync(func() { - // Save the current state (if any) if the children container is not - // hollow. - if !c.IsHollow() { - restore := c.saveSelectedRow() - defer restore() + gts.ExecAsync(func() { c.SetServersUnsafe(servers) }) +} + +func (c *Children) SetServersUnsafe(servers []cchat.Server) { + // Save the current state (if any) if the children container is not + // hollow. + if !c.IsHollow() { + restore := c.saveSelectedRow() + defer restore() + } + + // Reset before inserting new servers. + c.Reset() + + // Insert hollow servers. + c.Rows = make([]*ServerRow, len(servers)) + for i, server := range servers { + if server == nil { + log.Panicln("one of given servers in SetServers is nil at ", i) } + c.Rows[i] = NewHollowServer(c, server, c.rowctrl) + } - // Reset before inserting new servers. - c.Reset() - - // Insert hollow servers. - c.Rows = make([]*ServerRow, len(servers)) - for i, server := range servers { - if server == nil { - log.Panicln("one of given servers in SetServers is nil at ", i) - } - c.Rows[i] = NewHollowServer(c, server, c.rowctrl) - } - - // We should not unhollow everything here, but rather on uncollapse. - // Since the root node is always unhollow, calls to this function will - // pass the hollow test and unhollow its children nodes. That should not - // happen. - }) + // We should not unhollow everything here, but rather on uncollapse. + // Since the root node is always unhollow, calls to this function will + // pass the hollow test and unhollow its children nodes. That should not + // happen. } func (c *Children) findID(id cchat.ID) (int, *ServerRow) { @@ -196,35 +198,37 @@ func (c *Children) insertAt(row *ServerRow, i int) { } func (c *Children) UpdateServer(update cchat.ServerUpdate) { - gts.ExecAsync(func() { - prevID, replace := update.PreviousID() + gts.ExecAsync(func() { c.UpdateServerUnsafe(update) }) +} - // TODO: I don't think this code unhollows a new server. - var newServer = NewHollowServer(c, update, c.rowctrl) - var i, oldRow = c.findID(prevID) +func (c *Children) UpdateServerUnsafe(update cchat.ServerUpdate) { + prevID, replace := update.PreviousID() - // If we're appending a new row, then replace is false. - if !replace { - // Increment the old row's index so we know where to insert. - c.insertAt(newServer, i+1) - return - } + // TODO: I don't think this code unhollows a new server. + var newServer = NewHollowServer(c, update, c.rowctrl) + var i, oldRow = c.findID(prevID) - // Only update the server if the old row was found. - if oldRow == nil { - return - } + // If we're appending a new row, then replace is false. + if !replace { + // Increment the old row's index so we know where to insert. + c.insertAt(newServer, i+1) + return + } - c.Rows[i] = newServer + // Only update the server if the old row was found. + if oldRow == nil { + return + } - if !c.IsHollow() { - // Update the UI as well. - // TODO: check if this reorder is correct. - c.Box.Remove(oldRow) - c.Box.Add(newServer) - c.Box.ReorderChild(newServer, i) - } - }) + c.Rows[i] = newServer + + if !c.IsHollow() { + // Update the UI as well. + // TODO: check if this reorder is correct. + oldRow.Destroy() + c.Box.Add(newServer) + c.Box.ReorderChild(newServer, i) + } } // LoadAll forces all children rows to be unhollowed (initialized). It does diff --git a/internal/ui/service/session/server/server.go b/internal/ui/service/session/server/server.go index 0ee19c1..ed8096e 100644 --- a/internal/ui/service/session/server/server.go +++ b/internal/ui/service/session/server/server.go @@ -135,7 +135,7 @@ func (r *ServerRow) Init() { r.SetIconer(r.Server) // Connect the destroyer, if any. - r.Connect("destroy", r.cancelUnread) + r.Connect("destroy", func(interface{}) { r.cancelUnread() }) // Restore the read state. r.Button.SetUnreadUnsafe(r.unread, r.mentioned) // update with state @@ -269,11 +269,8 @@ func (r *ServerRow) load(finish func(error)) { // Reset clears off all children servers. It's a no-op if there are none. func (r *ServerRow) Reset() { if r.children != nil { - // Remove everything from the children container. r.children.Reset() - - // Remove the children container itself. - r.Box.Remove(r.children) + r.children.Destroy() } // Reset the state. diff --git a/internal/ui/service/session/servers.go b/internal/ui/service/session/servers.go index d404b89..41bd4f9 100644 --- a/internal/ui/service/session/servers.go +++ b/internal/ui/service/session/servers.go @@ -17,6 +17,13 @@ import ( const FaceSize = 48 // gtk.ICON_SIZE_DIALOG const ListWidth = 200 +// SessionController extends server.Controller to add needed methods that the +// specific top-level servers container needs. +type SessionController interface { + server.Controller + ClearMessenger() +} + // Servers wraps around a list of servers inherited from Children. It's the // container that's displayed on the right of the service sidebar. type Servers struct { @@ -24,6 +31,8 @@ type Servers struct { Children *server.Children spinner *spinner.Boxed // non-nil if loading. + ctrl SessionController + // state ServerList cchat.Lister } @@ -35,7 +44,7 @@ var toplevelCSS = primitives.PrepareClassCSS("top-level", ` } `) -func NewServers(p traverse.Breadcrumber, ctrl server.Controller) *Servers { +func NewServers(p traverse.Breadcrumber, ctrl SessionController) *Servers { c := server.NewChildren(p, ctrl) c.SetMarginStart(0) // children is top level; there is no main row c.SetVExpand(true) @@ -47,6 +56,7 @@ func NewServers(p traverse.Breadcrumber, ctrl server.Controller) *Servers { return &Servers{ Box: b, Children: c, + ctrl: ctrl, } } @@ -88,7 +98,7 @@ func (s *Servers) load() { s.setLoading() go func() { - err := s.ServerList.Servers(s.Children) + err := s.ServerList.Servers(s) gts.ExecAsync(func() { if err != nil { s.setFailed(err) @@ -99,6 +109,22 @@ func (s *Servers) load() { }() } +// SetServers is reserved for cchat.ServersContainer. +func (s *Servers) SetServers(servers []cchat.Server) { + gts.ExecAsync(func() { + s.Children.SetServersUnsafe(servers) + + if servers == nil { + s.ctrl.ClearMessenger() + } + }) +} + +// SetServers is reserved for cchat.ServersContainer. +func (s *Servers) UpdateServer(update cchat.ServerUpdate) { + gts.ExecAsync(func() { s.Children.UpdateServerUnsafe(update) }) +} + // setDone changes the view to show the servers. func (s *Servers) setDone() { primitives.RemoveChildren(s) @@ -139,7 +165,7 @@ func (s *Servers) setFailed(err error) { // Create a retry button. btn, _ := gtk.ButtonNewFromIconName("view-refresh-symbolic", gtk.ICON_SIZE_DIALOG) btn.Show() - btn.Connect("clicked", s.load) + btn.Connect("clicked", func(interface{}) { s.load() }) // Create a bottom label for the error itself. lerr, _ := gtk.LabelNew("") diff --git a/internal/ui/service/session/session.go b/internal/ui/service/session/session.go index fc47ead..242b533 100644 --- a/internal/ui/service/session/session.go +++ b/internal/ui/service/session/session.go @@ -22,16 +22,23 @@ import ( "github.com/pkg/errors" ) -// Servicer extends server.RowController to add session. type Servicer interface { // Service asks the controller for its service. Service() cchat.Service +} + +// Controller extends server.Controller to add session parameters. +type Controller interface { + Servicer + // OnSessionDisconnect is called before a session is disconnected. This // function is used for cleanups. OnSessionDisconnect(*Row) // SessionSelected is called when the row is clicked. The parent container // should change the views to show this session's *Servers. SessionSelected(*Row) + // ClearMessenger is called when a nil slice of servers is set. + ClearMessenger(*Row) // MessengerSelected is called when a server that can display messages (aka // implements Messenger) is called. MessengerSelected(*Row, *server.ServerRow) @@ -53,13 +60,13 @@ type Row struct { iconBox *gtk.EventBox icon *rich.Icon // nillable + ctrl Controller parentcrumb traverse.Breadcrumber Session cchat.Session // state; nilable sessionID string Servers *Servers // accessed by View for the right view - svcctrl Servicer ActionsMenu *actions.Menu // session.* @@ -129,22 +136,22 @@ func newIcon(img rich.RoundIconContainer) *rich.Icon { return icon } -func New(parent traverse.Breadcrumber, ses cchat.Session, ctrl Servicer) *Row { +func New(parent traverse.Breadcrumber, ses cchat.Session, ctrl Controller) *Row { row := newRow(parent, text.Rich{}, ctrl) row.SetSession(ses) return row } -func NewLoading(parent traverse.Breadcrumber, id, name string, ctrl Servicer) *Row { +func NewLoading(parent traverse.Breadcrumber, id, name string, ctrl Controller) *Row { row := newRow(parent, text.Rich{Content: name}, ctrl) row.sessionID = id row.SetLoading() return row } -func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Servicer) *Row { +func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Controller) *Row { row := &Row{ - svcctrl: ctrl, + ctrl: ctrl, parentcrumb: parent, } @@ -169,7 +176,7 @@ func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Servicer) *Row { row.ActionsMenu.InsertActionGroup(row) // Bind right clicks and show a popover menu on such event. - row.iconBox.Connect("button-press-event", func(_ gtk.IWidget, ev *gdk.Event) { + row.iconBox.Connect("button-press-event", func(_ interface{}, ev *gdk.Event) { if gts.EventIsRightClick(ev) { row.ActionsMenu.Popup(row) } @@ -242,6 +249,10 @@ func (r *Row) Breadcrumb() string { return r.Session.Name().Content } +func (r *Row) ClearMessenger() { + r.ctrl.ClearMessenger(r) +} + // Activate executes whatever needs to be done. If the row has failed, then this // method will reconnect. If the row is already loaded, then SessionSelected // will be called. @@ -257,7 +268,7 @@ func (r *Row) Activate() { } // Display the empty server list first, then try and reconnect. - r.svcctrl.SessionSelected(r) + r.ctrl.SessionSelected(r) } // SetLoading sets the session button to have a spinner circle. DO NOT CONFUSE @@ -371,13 +382,13 @@ func (r *Row) SetSession(ses cchat.Session) { } func (r *Row) MessengerSelected(sr *server.ServerRow) { - r.svcctrl.MessengerSelected(r, sr) + r.ctrl.MessengerSelected(r, sr) } // RemoveSession removes itself from the session list. func (r *Row) RemoveSession() { // Remove the session off the list. - r.svcctrl.RemoveSession(r) + r.ctrl.RemoveSession(r) var session = r.Session if session == nil { @@ -404,7 +415,7 @@ func (r *Row) ReconnectSession() { // Set the row as loading. r.SetLoading() // Try to restore the session. - r.svcctrl.RestoreSession(r, r.sessionID) + r.ctrl.RestoreSession(r, r.sessionID) } // DisconnectSession disconnects the current session. It does nothing if the row @@ -416,7 +427,7 @@ func (r *Row) DisconnectSession() { } // Call the disconnect function from the controller first. - r.svcctrl.OnSessionDisconnect(r) + r.ctrl.OnSessionDisconnect(r) // Copy the session to avoid data race and allow us to reset. session := r.Session diff --git a/internal/ui/service/view.go b/internal/ui/service/view.go index aea582d..a1264c9 100644 --- a/internal/ui/service/view.go +++ b/internal/ui/service/view.go @@ -13,6 +13,8 @@ import ( type Controller interface { // SessionSelected is called when SessionSelected(svc *Service, srow *session.Row) + // ClearMessenger is called when a nil slice of servers is set. + ClearMessenger(*session.Row) // MessengerSelected is wrapped around session's MessengerSelected. MessengerSelected(*session.Row, *server.ServerRow) // AuthenticateSession is called to spawn the authentication dialog. diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 98805d1..4d33be4 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -142,12 +142,20 @@ func (app *App) SessionSelected(svc *service.Service, ses *session.Row) { app.MessageView.Reset() } +func (app *App) ClearMessenger(ses *session.Row) { + if app.MessageView.SessionID() == ses.Session.ID() { + return + } +} + func (app *App) MessengerSelected(ses *session.Row, srv *server.ServerRow) { // Change to the message view. app.Leaflet.SetVisibleChild(app.MessageView) // Assert that the new server is not the same one. - if app.MessageView.ServerID() == srv.Server.ID() { + if app.MessageView.SessionID() == ses.Session.ID() && + app.MessageView.ServerID() == srv.Server.ID() { + return } @@ -210,7 +218,7 @@ func (app *App) Close() { } func (app *App) Icon() *gdk.Pixbuf { - return icons.Logo256(0) + return icons.Logo256Pixbuf() } func (app *App) Menu() *glib.MenuModel { diff --git a/madvdontneed.go b/madvdontneed.go index 74b008d..0a7c2a7 100644 --- a/madvdontneed.go +++ b/madvdontneed.go @@ -6,10 +6,7 @@ import ( "log" "os" "os/exec" - "runtime/debug" - "strings" "syscall" - "time" ) // Inject madvdontneed=1 as soon as possible. @@ -39,16 +36,3 @@ var _ = func() struct{} { os.Exit(0) return struct{}{} }() - -func init() { - // Aggressive memory freeing you asked, so aggressive memory freeing we will - // deliver. - if strings.Contains(os.Getenv("GODEBUG"), "madvdontneed=1") { - go func() { - log.Println("Now attempting to free memory every 5s... (madvdontneed=1)") - for range time.Tick(5 * time.Second) { - debug.FreeOSMemory() - } - }() - } -} diff --git a/main.go b/main.go index 76ba7ea..51cb2f9 100644 --- a/main.go +++ b/main.go @@ -1,16 +1,31 @@ package main import ( + "runtime" + "time" + "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/log" "github.com/diamondburned/cchat-gtk/internal/ui" "github.com/diamondburned/cchat-gtk/internal/ui/config" "github.com/diamondburned/cchat/services" + // _ "github.com/diamondburned/gotk3-tcmalloc" + // "github.com/diamondburned/gotk3-tcmalloc/heapprofiler" + _ "github.com/diamondburned/cchat-discord" _ "github.com/diamondburned/cchat-mock" ) +func init() { + go func() { + // If you GC more, you have shorter STWs. Easy. + for range time.Tick(time.Second) { + runtime.GC() + } + }() +} + func main() { gts.Main(func() gts.MainApplication { var app = ui.NewApplication() @@ -31,6 +46,9 @@ func main() { // Restore the configs. config.Restore() + // heapprofiler.Start("/tmp/cchat-gtk") + // gts.App.Window.Window.Connect("destroy", heapprofiler.Stop) + return app }) } diff --git a/shell.nix b/shell.nix index 56e87d8..4b7eefc 100644 --- a/shell.nix +++ b/shell.nix @@ -1,28 +1,42 @@ { pkgs ? import {} }: -let libhandy = pkgs.libhandy.overrideAttrs(old: { - name = "libhandy-1.0.1"; - src = builtins.fetchGit { - url = "https://gitlab.gnome.org/GNOME/libhandy.git"; - rev = "5cee0927b8b39dea1b2a62ec6d19169f73ba06c6"; - }; - patches = []; +let nostrip = pkg: pkgs.enableDebugging (pkg.overrideAttrs(old: { + dontStrip = true; + doCheck = false; + NIX_CFLAGS_COMPILE = (old.NIX_CFLAGS_COMPILE or "") + " -g"; + })); - buildInputs = old.buildInputs ++ (with pkgs; [ - gnome3.librsvg - gdk-pixbuf - ]); -}); + libhandy = pkgs.libhandy.overrideAttrs(old: { + name = "libhandy-1.0.1"; + src = builtins.fetchGit { + url = "https://gitlab.gnome.org/GNOME/libhandy.git"; + rev = "5cee0927b8b39dea1b2a62ec6d19169f73ba06c6"; + }; + patches = []; + + buildInputs = old.buildInputs ++ (with pkgs; [ + (nostrip gnome3.librsvg) + (nostrip gdk-pixbuf) + ]); + }); in pkgs.stdenv.mkDerivation rec { name = "cchat-gtk"; version = "0.0.2"; - buildInputs = [ libhandy ] ++ (with pkgs; [ - gnome3.gspell gnome3.glib gnome3.gtk - ]); + buildInputs = [ + (nostrip libhandy) + (nostrip pkgs.gnome3.gspell) + (nostrip pkgs.gnome3.glib) + (nostrip pkgs.gnome3.gtk) + ]; nativeBuildInputs = with pkgs; [ pkgconfig go + gperftools ]; + + # Debug flags. + CGO_CFLAGS = "-g"; + CGO_CXXFLAGS = "-g"; }