diff --git a/internal/gts/gts.go b/internal/gts/gts.go index f927690..63b3809 100644 --- a/internal/gts/gts.go +++ b/internal/gts/gts.go @@ -182,13 +182,28 @@ func ExecSync(fn func()) <-chan struct{} { return ch } +// DoAfter calls f after the given duration in the Gtk main loop. +func DoAfter(d time.Duration, f func()) { + DoAfterMs(uint(d.Milliseconds()), f) +} + +// 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) + } +} + // 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 }, - ) + return AfterMsFunc(uint(d.Milliseconds()), f) +} + +// 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) } diff --git a/internal/gts/httputil/image.go b/internal/gts/httputil/image.go index 026fd56..bb9fa56 100644 --- a/internal/gts/httputil/image.go +++ b/internal/gts/httputil/image.go @@ -7,41 +7,31 @@ import ( "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/imgutil" "github.com/gotk3/gotk3/gdk" - "github.com/gotk3/gotk3/glib" "github.com/pkg/errors" ) // TODO: type ImageContainer interface { + primitives.Connector + SetFromPixbuf(*gdk.Pixbuf) SetFromAnimation(*gdk.PixbufAnimation) GetSizeRequest() (w, h int) - Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error) } // AsyncImage loads an image. This method uses the cache. -func AsyncImage(img ImageContainer, url string, procs ...imgutil.Processor) { - asyncImage(img, url, procs) -} +func AsyncImage(ctx context.Context, + img ImageContainer, url string, procs ...imgutil.Processor) { -// AsyncImageSized resizes using GdkPixbuf. This method uses the cache. -func AsyncImageSized(img ImageContainer, url string, procs ...imgutil.Processor) { - asyncImage(img, url, procs) -} - -func asyncImage(img ImageContainer, url string, procs []imgutil.Processor) { if url == "" { return } - // // Add a processor to resize. - // procs = append(procs, imgutil.Resize(w, h)) - - ctx, cancel := context.WithCancel(context.Background()) - connectDestroyer(img, cancel) + ctx = primitives.HandleDestroyCtx(ctx, img) gif := strings.Contains(url, ".gif") @@ -65,12 +55,12 @@ func asyncImage(img ImageContainer, url string, procs []imgutil.Processor) { go syncImage(ctx, l, url, procs, gif) } -func connectDestroyer(img ImageContainer, cancel func()) { - img.Connect("destroy", func() { - cancel() - img.SetFromPixbuf(nil) - }) -} +// func connectDestroyer(img ImageContainer, cancel func()) { +// img.Connect("destroy", func() { +// cancel() +// img.SetFromPixbuf(nil) +// }) +// } func areaPreparedFn(ctx context.Context, img ImageContainer, gif bool) func(l *gdk.PixbufLoader) { return func(l *gdk.PixbufLoader) { diff --git a/internal/ui/messages/container/cozy/message_full.go b/internal/ui/messages/container/cozy/message_full.go index 74a692c..4e395c0 100644 --- a/internal/ui/messages/container/cozy/message_full.go +++ b/internal/ui/messages/container/cozy/message_full.go @@ -187,21 +187,22 @@ func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage { type Avatar struct { roundimage.Button - url string + image *roundimage.StaticImage + url string } func NewAvatar() *Avatar { img, _ := roundimage.NewStaticImage(nil, 0) + img.SetSizeRequest(AvatarSize, AvatarSize) img.Show() avatar, _ := roundimage.NewCustomButton(img) avatar.SetVAlign(gtk.ALIGN_START) - avatar.Image.SetSizeRequest(AvatarSize, AvatarSize) // Default icon. primitives.SetImageIcon(img, "user-available-symbolic", AvatarSize) - return &Avatar{*avatar, ""} + return &Avatar{*avatar, img, ""} } // SetURL updates the Avatar to be that URL. It does nothing if URL is empty or @@ -215,7 +216,7 @@ func (a *Avatar) SetURL(url string) { } a.url = url - httputil.AsyncImageSized(a.Image, url) + a.image.SetImageURL(url) } // ManuallySetURL sets the URL without downloading the image. It assumes the diff --git a/internal/ui/messages/input/username/username.go b/internal/ui/messages/input/username/username.go index 638bfc8..f514ed0 100644 --- a/internal/ui/messages/input/username/username.go +++ b/internal/ui/messages/input/username/username.go @@ -40,24 +40,13 @@ var usernameCSS = primitives.PrepareCSS(` `) func NewContainer() *Container { - avatar := rich.NewIcon(AvatarSize) - avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize) - avatar.Show() - - label := rich.NewLabel(text.Rich{}) - label.SetMaxWidthChars(35) - label.Show() - box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5) - box.PackStart(avatar, false, false, 0) - box.PackStart(label, false, false, 0) box.Show() primitives.AddClass(box, "username-view") primitives.AttachCSS(box, usernameCSS) rev, _ := gtk.RevealerNew() - rev.SetRevealChild(false) rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) rev.SetTransitionDuration(50) rev.Add(box) @@ -67,12 +56,13 @@ func NewContainer() *Container { // thread. currentRevealer = rev.SetRevealChild - return &Container{ + container := Container{ Revealer: rev, main: box, - avatar: avatar, - label: label, } + container.Reset() + + return &container } func (u *Container) SetRevealChild(reveal bool) { @@ -87,8 +77,18 @@ func (u *Container) shouldReveal() bool { func (u *Container) Reset() { u.SetRevealChild(false) - u.avatar.Reset() - u.label.Reset() + + u.avatar = rich.NewIcon(AvatarSize) + u.avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize) + u.avatar.Show() + + u.label = rich.NewLabel(text.Rich{}) + u.label.SetMaxWidthChars(35) + u.label.Show() + + primitives.RemoveChildren(u.main) + u.main.PackStart(u.avatar, false, false, 0) + u.main.PackStart(u.label, false, false, 0) } // Update is not thread-safe. diff --git a/internal/ui/messages/sadface/sadface.go b/internal/ui/messages/sadface/sadface.go index 64d39c9..450f000 100644 --- a/internal/ui/messages/sadface/sadface.go +++ b/internal/ui/messages/sadface/sadface.go @@ -29,6 +29,7 @@ func New(parent gtk.IWidget, placeholder gtk.IWidget) *FaceView { b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) stack, _ := gtk.StackNew() + stack.SetTransitionDuration(55) stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE) stack.AddNamed(parent, "main") stack.AddNamed(placeholder, "placeholder") diff --git a/internal/ui/messages/view.go b/internal/ui/messages/view.go index d8bc03a..d7d1b0b 100644 --- a/internal/ui/messages/view.go +++ b/internal/ui/messages/view.go @@ -278,13 +278,21 @@ func (v *View) MemberListUpdated(c *memberlist.Container) { // JoinServer is not thread-safe, but it calls backend functions asynchronously. func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) { - // Reset before setting. - v.Reset() - // Set the screen to loading. 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() + // Get the messenger once. var messenger = server.AsMessenger() // Exit if this server is not a messenger. diff --git a/internal/ui/primitives/completion/completer.go b/internal/ui/primitives/completion/completer.go index a714b3d..181d429 100644 --- a/internal/ui/primitives/completion/completer.go +++ b/internal/ui/primitives/completion/completer.go @@ -1,6 +1,7 @@ package completion import ( + "context" "fmt" "github.com/diamondburned/cchat" @@ -234,7 +235,7 @@ func (c *Completer) update() []gtk.IWidget { pps = ppIcon } - httputil.AsyncImageSized(img, entry.IconURL, pps...) + httputil.AsyncImage(context.Background(), img, entry.IconURL, pps...) } widgets[i] = b diff --git a/internal/ui/primitives/primitives.go b/internal/ui/primitives/primitives.go index 9407d4f..5a0281d 100644 --- a/internal/ui/primitives/primitives.go +++ b/internal/ui/primitives/primitives.go @@ -1,6 +1,7 @@ package primitives import ( + "context" "runtime/debug" "github.com/diamondburned/cchat-gtk/internal/gts" @@ -168,6 +169,12 @@ type Connector interface { Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error) } +func HandleDestroyCtx(ctx context.Context, connector Connector) context.Context { + ctx, cancel := context.WithCancel(ctx) + connector.Connect("destroy", cancel) + return ctx +} + func BindMenu(connector Connector, menu *gtk.Menu) { connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) { if gts.EventIsRightClick(ev) { diff --git a/internal/ui/primitives/roundimage/avatar.go b/internal/ui/primitives/roundimage/avatar.go index 0b2e85b..36d685e 100644 --- a/internal/ui/primitives/roundimage/avatar.go +++ b/internal/ui/primitives/roundimage/avatar.go @@ -1,6 +1,8 @@ package roundimage import ( + "context" + "github.com/diamondburned/cchat-gtk/internal/gts/httputil" "github.com/diamondburned/handy" "github.com/gotk3/gotk3/gdk" @@ -24,6 +26,7 @@ func TrySetText(imager Imager, text string) { type Avatar struct { handy.Avatar pixbuf *gdk.Pixbuf + url string size int } @@ -57,33 +60,48 @@ func (a *Avatar) SetSizeRequest(w, h int) { min = h } + a.size = min a.Avatar.SetSize(min) a.Avatar.SetSizeRequest(w, h) } func (a *Avatar) loadFunc(size int) *gdk.Pixbuf { - if a.pixbuf == nil { - a.size = size + // No URL, draw nothing. + if a.url == "" { return nil } - if a.size != size { - a.size = size - - p, err := a.pixbuf.ScaleSimple(size, size, gdk.INTERP_HYPER) - if err != nil { - return a.pixbuf - } - - a.pixbuf = p + if a.pixbuf != nil && a.size == size { + return a.pixbuf } - return a.pixbuf + // Refetch and rescale. + a.size = size + // Technically, this will recurse. However, we're changing the size, so + // eventually it should stop. + httputil.AsyncImage(context.Background(), a, a.url) + + if a.pixbuf == nil { + return nil + } + + // Temporarily resize for now. + p, err := a.pixbuf.ScaleSimple(size, size, gdk.INTERP_HYPER) + if err != nil { + p = a.pixbuf + } + + return p } // SetRadius is a no-op. func (a *Avatar) SetRadius(float64) {} +func (a *Avatar) SetImageURL(url string) { + a.url = url + a.Avatar.SetImageLoadFunc(a.loadFunc) +} + // SetFromPixbuf sets the pixbuf. func (a *Avatar) SetFromPixbuf(pb *gdk.Pixbuf) { a.pixbuf = pb diff --git a/internal/ui/primitives/roundimage/roundimage.go b/internal/ui/primitives/roundimage/roundimage.go index 3d9523a..9509890 100644 --- a/internal/ui/primitives/roundimage/roundimage.go +++ b/internal/ui/primitives/roundimage/roundimage.go @@ -1,10 +1,12 @@ package roundimage import ( + "context" "math" "github.com/diamondburned/cchat-gtk/internal/gts/httputil" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" + "github.com/diamondburned/imgutil" "github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gtk" @@ -40,6 +42,7 @@ type Imager interface { type Image struct { *gtk.Image Radius float64 + procs []imgutil.Processor } var _ Imager = (*Image)(nil) @@ -60,10 +63,19 @@ func NewImage(radius float64) (*Image, error) { return image, nil } +func (i *Image) AddProcessor(procs ...imgutil.Processor) { + i.procs = append(i.procs, procs...) +} + func (i *Image) GetImage() *gtk.Image { return i.Image } +func (i *Image) SetImageURL(url string) { + // No dynamic sizing support; yolo. + httputil.AsyncImage(context.Background(), i, url, i.procs...) +} + func (i *Image) SetRadius(r float64) { i.Radius = r } diff --git a/internal/ui/primitives/roundimage/static.go b/internal/ui/primitives/roundimage/static.go index f68df50..cefa9ce 100644 --- a/internal/ui/primitives/roundimage/static.go +++ b/internal/ui/primitives/roundimage/static.go @@ -1,6 +1,8 @@ package roundimage import ( + "context" + "github.com/diamondburned/cchat-gtk/internal/gts/httputil" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/gotk3/gotk3/gdk" @@ -9,6 +11,7 @@ import ( // StaticImage is an image that only plays a GIF if it's hovered on top of. type StaticImage struct { *Image + animating bool animation *gdk.PixbufAnimation } @@ -24,7 +27,7 @@ func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage, return nil, err } - var s = &StaticImage{i, nil} + var s = &StaticImage{i, false, nil} if parent != nil { s.ConnectHandlers(parent) } @@ -34,17 +37,24 @@ func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage, func (s *StaticImage) ConnectHandlers(connector primitives.Connector) { connector.Connect("enter-notify-event", func() { - if s.animation != nil { + if s.animation != nil && !s.animating { + s.animating = true s.Image.SetFromAnimation(s.animation) } }) connector.Connect("leave-notify-event", func() { - if s.animation != nil { + if s.animation != nil && s.animating { + s.animating = false s.Image.SetFromPixbuf(s.animation.GetStaticImage()) } }) } +func (s *StaticImage) SetImageURL(url string) { + // No dynamic sizing support; yolo. + httputil.AsyncImage(context.Background(), s, url, s.Image.procs...) +} + func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) { s.animation = nil s.Image.SetFromPixbuf(pb) diff --git a/internal/ui/rich/async.go b/internal/ui/rich/async.go index 6ff3ce3..5827e06 100644 --- a/internal/ui/rich/async.go +++ b/internal/ui/rich/async.go @@ -3,8 +3,6 @@ package rich import ( "context" "log" - "reflect" - "time" "github.com/diamondburned/cchat-gtk/internal/gts" ) @@ -46,63 +44,3 @@ func AsyncUse(r Reuser, fn AsyncUser) { }, 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)}) -} diff --git a/internal/ui/rich/image.go b/internal/ui/rich/image.go index 51a3261..6efab0c 100644 --- a/internal/ui/rich/image.go +++ b/internal/ui/rich/image.go @@ -9,7 +9,6 @@ import ( "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage" "github.com/diamondburned/cchat/text" - "github.com/diamondburned/imgutil" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gtk" "github.com/pkg/errors" @@ -19,10 +18,12 @@ type IconerFn = func(context.Context, cchat.IconContainer) (func(), error) type RoundIconContainer interface { gtk.IWidget - httputil.ImageContainer + primitives.ImageIconSetter roundimage.RadiusSetter + SetImageURL(url string) + GetStorageType() gtk.ImageType GetPixbuf() *gdk.Pixbuf GetAnimation() *gdk.PixbufAnimation @@ -35,11 +36,9 @@ var ( // Icon represents a rounded image container. type Icon struct { - *gtk.Revealer - Image RoundIconContainer - procs []imgutil.Processor + *gtk.Revealer // TODO move out - r *Reusable + Image RoundIconContainer // state url string @@ -49,13 +48,13 @@ const DefaultIconSize = 16 var _ cchat.IconContainer = (*Icon)(nil) -func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon { +func NewIcon(sizepx int) *Icon { img, _ := roundimage.NewImage(0) img.Show() - return NewCustomIcon(img, sizepx, procs...) + return NewCustomIcon(img, sizepx) } -func NewCustomIcon(img RoundIconContainer, sizepx int, procs ...imgutil.Processor) *Icon { +func NewCustomIcon(img RoundIconContainer, sizepx int) *Icon { if sizepx == 0 { sizepx = DefaultIconSize } @@ -69,24 +68,12 @@ func NewCustomIcon(img RoundIconContainer, sizepx int, procs ...imgutil.Processo i := &Icon{ Revealer: rev, Image: img, - procs: procs, } i.SetSize(sizepx) - i.r = NewReusable(func(ni *nullIcon) { - i.SetIconUnsafe(ni.url) - }) return i } -// Reset wipes the state to be just after construction. -func (i *Icon) Reset() { - i.url = "" - i.r.Invalidate() // invalidate async fetching images - i.Revealer.SetRevealChild(false) - i.Image.SetFromPixbuf(nil) // destroy old pb -} - // URL is not thread-safe. func (i *Icon) URL() string { return i.url @@ -127,11 +114,6 @@ func (i *Icon) Size() int { return w } -// AddProcessors is not thread-safe. -func (i *Icon) AddProcessors(procs ...imgutil.Processor) { - i.procs = append(i.procs, procs...) -} - // SetIcon is thread-safe. func (i *Icon) SetIcon(url string) { gts.ExecAsync(func() { i.SetIconUnsafe(url) }) @@ -141,45 +123,48 @@ func (i *Icon) AsyncSetIconer(iconer cchat.Iconer, errwrap string) { // Reveal to show the placeholder. i.SetRevealChild(true) - AsyncUse(i.r, func(ctx context.Context) (interface{}, func(), error) { - ni := &nullIcon{} - f, err := iconer.Icon(ctx, ni) - return ni, f, errors.Wrap(err, errwrap) + // I have a hunch this will never work; as long as Go keeps a reference with + // iconer.Icon, then destroy will never be triggered. + ctx := primitives.HandleDestroyCtx(context.Background(), i) + gts.Async(func() (func(), error) { + f, err := iconer.Icon(ctx, i) + if err != nil { + return nil, errors.Wrap(err, "failed to load iconer") + } + + return func() { i.Connect("destroy", f) }, nil }) } // SetIconUnsafe is not thread-safe. func (i *Icon) SetIconUnsafe(url string) { - i.Image.SetRadius(0) // round + // Setting the radius here since we resetted it for a placeholder icon. + i.Image.SetRadius(0) i.SetRevealChild(true) i.url = url - i.updateAsync() + i.Image.SetImageURL(i.url) } -func (i *Icon) updateAsync() { - httputil.AsyncImageSized(i.Image, i.url, i.procs...) -} +// type EventIcon struct { +// *gtk.EventBox +// Icon *Icon +// } -type EventIcon struct { - *gtk.EventBox - Icon *Icon -} +// func NewEventIcon(sizepx int) *EventIcon { +// icn := NewIcon(sizepx) +// return WrapEventIcon(icn) +// } -func NewEventIcon(sizepx int, pp ...imgutil.Processor) *EventIcon { - icn := NewIcon(sizepx, pp...) - return WrapEventIcon(icn) -} +// func WrapEventIcon(icn *Icon) *EventIcon { +// icn.Show() +// evb, _ := gtk.EventBoxNew() +// evb.Add(icn) -func WrapEventIcon(icn *Icon) *EventIcon { - icn.Show() - evb, _ := gtk.EventBoxNew() - evb.Add(icn) - - return &EventIcon{ - EventBox: evb, - Icon: icn, - } -} +// return &EventIcon{ +// EventBox: evb, +// Icon: icn, +// } +// } type ToggleButtonImage struct { gtk.ToggleButton @@ -232,8 +217,3 @@ func NewCustomToggleButtonImage(img RoundIconContainer, content text.Rich) *Togg Box: box, } } - -func (t *ToggleButtonImage) Reset() { - t.Labeler.Reset() - t.Image.Reset() -} diff --git a/internal/ui/rich/label.go b/internal/ui/rich/label.go index 05e2c88..0836b4c 100644 --- a/internal/ui/rich/label.go +++ b/internal/ui/rich/label.go @@ -10,6 +10,7 @@ import ( "github.com/diamondburned/cchat/text" "github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/pango" + "github.com/pkg/errors" ) type Labeler interface { @@ -20,13 +21,11 @@ type Labeler interface { SetLabelUnsafe(text.Rich) GetLabel() text.Rich GetText() string - Reset() } // SuperLabeler represents a label that inherits the current labeler. type SuperLabeler interface { SetLabelUnsafe(text.Rich) - Reset() } type LabelerFn = func(context.Context, cchat.LabelContainer) (func(), error) @@ -35,9 +34,6 @@ type Label struct { gtk.Label Current text.Rich - // Reusable primitive. - r *Reusable - // super unexported field for inheritance super SuperLabeler } @@ -58,11 +54,6 @@ func NewLabel(content text.Rich) *Label { Current: content, } - // reusable primitive - l.r = NewReusable(func(nl *nullLabel) { - l.SetLabelUnsafe(nl.Rich) - }) - return l } @@ -80,23 +71,15 @@ func (l *Label) validsuper() bool { return !ok && l.super != nil } -// Reset wipes the state to be just after construction. If super is not nil, -// then it's reset as well. -func (l *Label) Reset() { - l.Current = text.Rich{} - l.r.Invalidate() - l.Label.SetText("") - - if l.validsuper() { - l.super.Reset() - } -} - func (l *Label) AsyncSetLabel(fn LabelerFn, info string) { - AsyncUse(l.r, func(ctx context.Context) (interface{}, func(), error) { - nl := &nullLabel{} - f, err := fn(ctx, nl) - return nl, f, err + ctx := primitives.HandleDestroyCtx(context.Background(), l) + gts.Async(func() (func(), error) { + f, err := fn(ctx, l) + if err != nil { + return nil, errors.Wrap(err, "failed to load iconer") + } + + return func() { l.Connect("destroy", f) }, nil }) } diff --git a/internal/ui/rich/labeluri/labeluri.go b/internal/ui/rich/labeluri/labeluri.go index ffb74bd..e8fd83c 100644 --- a/internal/ui/rich/labeluri/labeluri.go +++ b/internal/ui/rich/labeluri/labeluri.go @@ -1,6 +1,7 @@ package labeluri import ( + "context" "fmt" "html" "net/url" @@ -211,7 +212,7 @@ func popoverImg(url string, round bool) gtk.IWidget { img.SetHAlign(gtk.ALIGN_CENTER) img.Show() - httputil.AsyncImageSized(idl, url) + httputil.AsyncImage(context.Background(), idl, url) btn.SetHAlign(gtk.ALIGN_CENTER) btn.SetRelief(gtk.RELIEF_NONE) @@ -268,7 +269,7 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle) img.Show() // Asynchronously fetch the image. - httputil.AsyncImageSized(img, uri) + httputil.AsyncImage(context.Background(), img, uri) btn, _ := gtk.ButtonNew() btn.Add(img) diff --git a/internal/ui/rich/rich.go b/internal/ui/rich/rich.go index ec85151..d016599 100644 --- a/internal/ui/rich/rich.go +++ b/internal/ui/rich/rich.go @@ -20,10 +20,3 @@ type nullLabel struct { } func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t } - -// used for grabbing url without changing state -type nullIcon struct { - url string -} - -func (i *nullIcon) SetIcon(url string) { i.url = url } diff --git a/internal/ui/service/session/session.go b/internal/ui/service/session/session.go index dddeea6..fc47ead 100644 --- a/internal/ui/service/session/session.go +++ b/internal/ui/service/session/session.go @@ -22,9 +22,6 @@ import ( "github.com/pkg/errors" ) -const IconSize = 48 -const IconName = "face-plain-symbolic" - // Servicer extends server.RowController to add session. type Servicer interface { // Service asks the controller for its service. @@ -52,8 +49,9 @@ type Servicer interface { // Row represents a session row entry in the session List. type Row struct { *gtk.ListBoxRow - avatar *roundimage.Avatar - icon *rich.EventIcon // nilable + avatar *roundimage.Avatar + iconBox *gtk.EventBox + icon *rich.Icon // nillable parentcrumb traverse.Breadcrumber @@ -107,6 +105,10 @@ var rowCSS = primitives.PrepareClassCSS("session-row", 0.65 ), 0.85); } + + .session-row.failed { + background-color: alpha(red, 0.45); + } `) var rowIconCSS = primitives.PrepareClassCSS("session-icon", ` @@ -114,12 +116,19 @@ var rowIconCSS = primitives.PrepareClassCSS("session-icon", ` padding: 4px; margin: 0; } - - .session-icon.failed { - background-color: alpha(red, 0.45); - } `) +const IconSize = 48 +const IconName = "face-plain-symbolic" + +func newIcon(img rich.RoundIconContainer) *rich.Icon { + icon := rich.NewCustomIcon(img, IconSize) + icon.SetPlaceholderIcon(IconName, IconSize) + icon.ShowAll() + rowIconCSS(icon) + return icon +} + func New(parent traverse.Breadcrumber, ses cchat.Session, ctrl Servicer) *Row { row := newRow(parent, text.Rich{}, ctrl) row.SetSession(ses) @@ -143,13 +152,8 @@ func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Servicer) *Row { row.avatar.SetText(name.Content) row.avatar.Show() - icon := rich.NewCustomIcon(row.avatar, IconSize) - icon.Show() - - row.icon = rich.WrapEventIcon(icon) - row.icon.Icon.SetPlaceholderIcon(IconName, IconSize) - row.icon.Show() - rowIconCSS(row.icon.Icon) + row.iconBox, _ = gtk.EventBoxNew() + row.iconBox.Show() row.ListBoxRow, _ = gtk.ListBoxRowNew() row.ListBoxRow.Show() @@ -165,7 +169,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.icon.Connect("button-press-event", func(_ gtk.IWidget, ev *gdk.Event) { + row.iconBox.Connect("button-press-event", func(_ gtk.IWidget, ev *gdk.Event) { if gts.EventIsRightClick(ev) { row.ActionsMenu.Popup(row) } @@ -215,8 +219,13 @@ func (r *Row) Reset() { r.ActionsMenu.Reset() // wipe menu items r.ActionsMenu.AddAction("Remove", r.RemoveSession) + if r.icon == nil { + r.icon = newIcon(r.avatar) + r.iconBox.Add(r.icon) + } + // Set a lame placeholder icon. - r.icon.Icon.SetPlaceholderIcon("folder-remote-symbolic", IconSize) + r.icon.SetPlaceholderIcon("folder-remote-symbolic", IconSize) r.Session = nil r.cmder = nil @@ -258,13 +267,14 @@ func (r *Row) SetLoading() { r.Session = nil // Reset the icon. - r.icon.Icon.Reset() + primitives.RemoveChildren(r.iconBox) + r.icon = nil // Remove everything from the row, including the icon. primitives.RemoveChildren(r) // Remove the failed class. - primitives.RemoveClass(r.icon.Icon, "failed") + primitives.RemoveClass(r, "failed") // Add a loading circle. spin := spinner.New() @@ -287,15 +297,18 @@ func (r *Row) SetFailed(err error) { r.SetSensitive(true) // Remove everything off the row. primitives.RemoveChildren(r) - // Add the icon. - r.Add(r.icon) - // Set the button to a retry icon. - r.icon.Icon.SetPlaceholderIcon("view-refresh-symbolic", IconSize) - // Mark the icon as failed. - primitives.AddClass(r.icon.Icon, "failed") + // Mark the row as failed. + primitives.AddClass(r, "failed") - // SetFailed, but also add the callback to retry. - // r.Row.SetFailed(err, r.ReconnectSession) + if r.icon == nil { + r.icon = newIcon(r.avatar) + r.iconBox.Add(r.icon) + } + + // Add the icon. + r.Add(r.iconBox) + // Set the button to a retry icon. + r.icon.SetPlaceholderIcon("view-refresh-symbolic", IconSize) } func (r *Row) RestoreSession(res cchat.SessionRestorer, k keyring.Session) { @@ -318,18 +331,24 @@ func (r *Row) SetSession(ses cchat.Session) { r.Session = ses r.sessionID = ses.ID() r.SetTooltipMarkup(markup.Render(ses.Name())) - r.icon.Icon.SetPlaceholderIcon(IconName, IconSize) r.avatar.SetText(ses.Name().Content) + if r.icon == nil { + r.icon = newIcon(r.avatar) + r.iconBox.Add(r.icon) + } + + r.icon.SetPlaceholderIcon(IconName, IconSize) + // If the session has an icon, then use it. if iconer := ses.AsIconer(); iconer != nil { - r.icon.Icon.AsyncSetIconer(iconer, "failed to set session icon") + r.icon.AsyncSetIconer(iconer, "failed to set session icon") } // Update to indicate that we're done. primitives.RemoveChildren(r) r.SetSensitive(true) - r.Add(r.icon) + r.Add(r.iconBox) // Bind extra menu items before loading. These items won't be clickable // during loading.