fixed scaling with image API refactor, minor tweaks

This commit is contained in:
diamondburned 2020-12-20 01:48:52 -08:00
parent 296346bbe7
commit d3048fe08c
17 changed files with 228 additions and 251 deletions

View File

@ -182,13 +182,28 @@ func ExecSync(fn func()) <-chan struct{} {
return ch 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 // AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
// main loop. // main loop.
func AfterFunc(d time.Duration, f func()) (stop func()) { func AfterFunc(d time.Duration, f func()) (stop func()) {
h, err := glib.TimeoutAdd( return AfterMsFunc(uint(d.Milliseconds()), f)
uint(d.Milliseconds()), }
func() bool { f(); return true },
) // 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 { if err != nil {
panic(err) panic(err)
} }

View File

@ -7,41 +7,31 @@ import (
"github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log" "github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/imgutil" "github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// TODO: // TODO:
type ImageContainer interface { type ImageContainer interface {
primitives.Connector
SetFromPixbuf(*gdk.Pixbuf) SetFromPixbuf(*gdk.Pixbuf)
SetFromAnimation(*gdk.PixbufAnimation) SetFromAnimation(*gdk.PixbufAnimation)
GetSizeRequest() (w, h int) GetSizeRequest() (w, h int)
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error)
} }
// AsyncImage loads an image. This method uses the cache. // AsyncImage loads an image. This method uses the cache.
func AsyncImage(img ImageContainer, url string, procs ...imgutil.Processor) { func AsyncImage(ctx context.Context,
asyncImage(img, url, procs) 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 == "" { if url == "" {
return return
} }
// // Add a processor to resize. ctx = primitives.HandleDestroyCtx(ctx, img)
// procs = append(procs, imgutil.Resize(w, h))
ctx, cancel := context.WithCancel(context.Background())
connectDestroyer(img, cancel)
gif := strings.Contains(url, ".gif") 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) go syncImage(ctx, l, url, procs, gif)
} }
func connectDestroyer(img ImageContainer, cancel func()) { // func connectDestroyer(img ImageContainer, cancel func()) {
img.Connect("destroy", func() { // img.Connect("destroy", func() {
cancel() // cancel()
img.SetFromPixbuf(nil) // img.SetFromPixbuf(nil)
}) // })
} // }
func areaPreparedFn(ctx context.Context, img ImageContainer, gif bool) func(l *gdk.PixbufLoader) { func areaPreparedFn(ctx context.Context, img ImageContainer, gif bool) func(l *gdk.PixbufLoader) {
return func(l *gdk.PixbufLoader) { return func(l *gdk.PixbufLoader) {

View File

@ -187,21 +187,22 @@ func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage {
type Avatar struct { type Avatar struct {
roundimage.Button roundimage.Button
url string image *roundimage.StaticImage
url string
} }
func NewAvatar() *Avatar { func NewAvatar() *Avatar {
img, _ := roundimage.NewStaticImage(nil, 0) img, _ := roundimage.NewStaticImage(nil, 0)
img.SetSizeRequest(AvatarSize, AvatarSize)
img.Show() img.Show()
avatar, _ := roundimage.NewCustomButton(img) avatar, _ := roundimage.NewCustomButton(img)
avatar.SetVAlign(gtk.ALIGN_START) avatar.SetVAlign(gtk.ALIGN_START)
avatar.Image.SetSizeRequest(AvatarSize, AvatarSize)
// Default icon. // Default icon.
primitives.SetImageIcon(img, "user-available-symbolic", AvatarSize) 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 // 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 a.url = url
httputil.AsyncImageSized(a.Image, url) a.image.SetImageURL(url)
} }
// ManuallySetURL sets the URL without downloading the image. It assumes the // ManuallySetURL sets the URL without downloading the image. It assumes the

View File

@ -40,24 +40,13 @@ var usernameCSS = primitives.PrepareCSS(`
`) `)
func NewContainer() *Container { 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, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
box.PackStart(avatar, false, false, 0)
box.PackStart(label, false, false, 0)
box.Show() box.Show()
primitives.AddClass(box, "username-view") primitives.AddClass(box, "username-view")
primitives.AttachCSS(box, usernameCSS) primitives.AttachCSS(box, usernameCSS)
rev, _ := gtk.RevealerNew() rev, _ := gtk.RevealerNew()
rev.SetRevealChild(false)
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
rev.SetTransitionDuration(50) rev.SetTransitionDuration(50)
rev.Add(box) rev.Add(box)
@ -67,12 +56,13 @@ func NewContainer() *Container {
// thread. // thread.
currentRevealer = rev.SetRevealChild currentRevealer = rev.SetRevealChild
return &Container{ container := Container{
Revealer: rev, Revealer: rev,
main: box, main: box,
avatar: avatar,
label: label,
} }
container.Reset()
return &container
} }
func (u *Container) SetRevealChild(reveal bool) { func (u *Container) SetRevealChild(reveal bool) {
@ -87,8 +77,18 @@ func (u *Container) shouldReveal() bool {
func (u *Container) Reset() { func (u *Container) Reset() {
u.SetRevealChild(false) 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. // Update is not thread-safe.

View File

@ -29,6 +29,7 @@ func New(parent gtk.IWidget, placeholder gtk.IWidget) *FaceView {
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
stack, _ := gtk.StackNew() stack, _ := gtk.StackNew()
stack.SetTransitionDuration(55)
stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE) stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
stack.AddNamed(parent, "main") stack.AddNamed(parent, "main")
stack.AddNamed(placeholder, "placeholder") stack.AddNamed(placeholder, "placeholder")

View File

@ -278,13 +278,21 @@ func (v *View) MemberListUpdated(c *memberlist.Container) {
// JoinServer is not thread-safe, but it calls backend functions asynchronously. // JoinServer is not thread-safe, but it calls backend functions asynchronously.
func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) { func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) {
// Reset before setting.
v.Reset()
// Set the screen to loading. // Set the screen to loading.
v.FaceView.SetLoading() v.FaceView.SetLoading()
v.ctrl.OnMessageBusy() 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. // Get the messenger once.
var messenger = server.AsMessenger() var messenger = server.AsMessenger()
// Exit if this server is not a messenger. // Exit if this server is not a messenger.

View File

@ -1,6 +1,7 @@
package completion package completion
import ( import (
"context"
"fmt" "fmt"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
@ -234,7 +235,7 @@ func (c *Completer) update() []gtk.IWidget {
pps = ppIcon pps = ppIcon
} }
httputil.AsyncImageSized(img, entry.IconURL, pps...) httputil.AsyncImage(context.Background(), img, entry.IconURL, pps...)
} }
widgets[i] = b widgets[i] = b

View File

@ -1,6 +1,7 @@
package primitives package primitives
import ( import (
"context"
"runtime/debug" "runtime/debug"
"github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/gts"
@ -168,6 +169,12 @@ type Connector interface {
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error) 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) { func BindMenu(connector Connector, menu *gtk.Menu) {
connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) { connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
if gts.EventIsRightClick(ev) { if gts.EventIsRightClick(ev) {

View File

@ -1,6 +1,8 @@
package roundimage package roundimage
import ( import (
"context"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil" "github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/handy" "github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
@ -24,6 +26,7 @@ func TrySetText(imager Imager, text string) {
type Avatar struct { type Avatar struct {
handy.Avatar handy.Avatar
pixbuf *gdk.Pixbuf pixbuf *gdk.Pixbuf
url string
size int size int
} }
@ -57,33 +60,48 @@ func (a *Avatar) SetSizeRequest(w, h int) {
min = h min = h
} }
a.size = min
a.Avatar.SetSize(min) a.Avatar.SetSize(min)
a.Avatar.SetSizeRequest(w, h) a.Avatar.SetSizeRequest(w, h)
} }
func (a *Avatar) loadFunc(size int) *gdk.Pixbuf { func (a *Avatar) loadFunc(size int) *gdk.Pixbuf {
if a.pixbuf == nil { // No URL, draw nothing.
a.size = size if a.url == "" {
return nil return nil
} }
if a.size != size { if a.pixbuf != nil && a.size == size {
a.size = size return a.pixbuf
p, err := a.pixbuf.ScaleSimple(size, size, gdk.INTERP_HYPER)
if err != nil {
return a.pixbuf
}
a.pixbuf = p
} }
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. // SetRadius is a no-op.
func (a *Avatar) SetRadius(float64) {} func (a *Avatar) SetRadius(float64) {}
func (a *Avatar) SetImageURL(url string) {
a.url = url
a.Avatar.SetImageLoadFunc(a.loadFunc)
}
// SetFromPixbuf sets the pixbuf. // SetFromPixbuf sets the pixbuf.
func (a *Avatar) SetFromPixbuf(pb *gdk.Pixbuf) { func (a *Avatar) SetFromPixbuf(pb *gdk.Pixbuf) {
a.pixbuf = pb a.pixbuf = pb

View File

@ -1,10 +1,12 @@
package roundimage package roundimage
import ( import (
"context"
"math" "math"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil" "github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/cairo"
"github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
@ -40,6 +42,7 @@ type Imager interface {
type Image struct { type Image struct {
*gtk.Image *gtk.Image
Radius float64 Radius float64
procs []imgutil.Processor
} }
var _ Imager = (*Image)(nil) var _ Imager = (*Image)(nil)
@ -60,10 +63,19 @@ func NewImage(radius float64) (*Image, error) {
return image, nil return image, nil
} }
func (i *Image) AddProcessor(procs ...imgutil.Processor) {
i.procs = append(i.procs, procs...)
}
func (i *Image) GetImage() *gtk.Image { func (i *Image) GetImage() *gtk.Image {
return i.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) { func (i *Image) SetRadius(r float64) {
i.Radius = r i.Radius = r
} }

View File

@ -1,6 +1,8 @@
package roundimage package roundimage
import ( import (
"context"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil" "github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gdk" "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. // StaticImage is an image that only plays a GIF if it's hovered on top of.
type StaticImage struct { type StaticImage struct {
*Image *Image
animating bool
animation *gdk.PixbufAnimation animation *gdk.PixbufAnimation
} }
@ -24,7 +27,7 @@ func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage,
return nil, err return nil, err
} }
var s = &StaticImage{i, nil} var s = &StaticImage{i, false, nil}
if parent != nil { if parent != nil {
s.ConnectHandlers(parent) s.ConnectHandlers(parent)
} }
@ -34,17 +37,24 @@ func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage,
func (s *StaticImage) ConnectHandlers(connector primitives.Connector) { func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
connector.Connect("enter-notify-event", func() { connector.Connect("enter-notify-event", func() {
if s.animation != nil { if s.animation != nil && !s.animating {
s.animating = true
s.Image.SetFromAnimation(s.animation) s.Image.SetFromAnimation(s.animation)
} }
}) })
connector.Connect("leave-notify-event", func() { 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()) 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) { func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) {
s.animation = nil s.animation = nil
s.Image.SetFromPixbuf(pb) s.Image.SetFromPixbuf(pb)

View File

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

View File

@ -9,7 +9,6 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -19,10 +18,12 @@ type IconerFn = func(context.Context, cchat.IconContainer) (func(), error)
type RoundIconContainer interface { type RoundIconContainer interface {
gtk.IWidget gtk.IWidget
httputil.ImageContainer
primitives.ImageIconSetter primitives.ImageIconSetter
roundimage.RadiusSetter roundimage.RadiusSetter
SetImageURL(url string)
GetStorageType() gtk.ImageType GetStorageType() gtk.ImageType
GetPixbuf() *gdk.Pixbuf GetPixbuf() *gdk.Pixbuf
GetAnimation() *gdk.PixbufAnimation GetAnimation() *gdk.PixbufAnimation
@ -35,11 +36,9 @@ var (
// Icon represents a rounded image container. // Icon represents a rounded image container.
type Icon struct { type Icon struct {
*gtk.Revealer *gtk.Revealer // TODO move out
Image RoundIconContainer
procs []imgutil.Processor
r *Reusable Image RoundIconContainer
// state // state
url string url string
@ -49,13 +48,13 @@ const DefaultIconSize = 16
var _ cchat.IconContainer = (*Icon)(nil) var _ cchat.IconContainer = (*Icon)(nil)
func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon { func NewIcon(sizepx int) *Icon {
img, _ := roundimage.NewImage(0) img, _ := roundimage.NewImage(0)
img.Show() 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 { if sizepx == 0 {
sizepx = DefaultIconSize sizepx = DefaultIconSize
} }
@ -69,24 +68,12 @@ func NewCustomIcon(img RoundIconContainer, sizepx int, procs ...imgutil.Processo
i := &Icon{ i := &Icon{
Revealer: rev, Revealer: rev,
Image: img, Image: img,
procs: procs,
} }
i.SetSize(sizepx) i.SetSize(sizepx)
i.r = NewReusable(func(ni *nullIcon) {
i.SetIconUnsafe(ni.url)
})
return i 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. // URL is not thread-safe.
func (i *Icon) URL() string { func (i *Icon) URL() string {
return i.url return i.url
@ -127,11 +114,6 @@ func (i *Icon) Size() int {
return w return w
} }
// AddProcessors is not thread-safe.
func (i *Icon) AddProcessors(procs ...imgutil.Processor) {
i.procs = append(i.procs, procs...)
}
// SetIcon is thread-safe. // SetIcon is thread-safe.
func (i *Icon) SetIcon(url string) { func (i *Icon) SetIcon(url string) {
gts.ExecAsync(func() { i.SetIconUnsafe(url) }) gts.ExecAsync(func() { i.SetIconUnsafe(url) })
@ -141,45 +123,48 @@ func (i *Icon) AsyncSetIconer(iconer cchat.Iconer, errwrap string) {
// Reveal to show the placeholder. // Reveal to show the placeholder.
i.SetRevealChild(true) i.SetRevealChild(true)
AsyncUse(i.r, func(ctx context.Context) (interface{}, func(), error) { // I have a hunch this will never work; as long as Go keeps a reference with
ni := &nullIcon{} // iconer.Icon, then destroy will never be triggered.
f, err := iconer.Icon(ctx, ni) ctx := primitives.HandleDestroyCtx(context.Background(), i)
return ni, f, errors.Wrap(err, errwrap) 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. // SetIconUnsafe is not thread-safe.
func (i *Icon) SetIconUnsafe(url string) { 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.SetRevealChild(true)
i.url = url i.url = url
i.updateAsync() i.Image.SetImageURL(i.url)
} }
func (i *Icon) updateAsync() { // type EventIcon struct {
httputil.AsyncImageSized(i.Image, i.url, i.procs...) // *gtk.EventBox
} // Icon *Icon
// }
type EventIcon struct { // func NewEventIcon(sizepx int) *EventIcon {
*gtk.EventBox // icn := NewIcon(sizepx)
Icon *Icon // return WrapEventIcon(icn)
} // }
func NewEventIcon(sizepx int, pp ...imgutil.Processor) *EventIcon { // func WrapEventIcon(icn *Icon) *EventIcon {
icn := NewIcon(sizepx, pp...) // icn.Show()
return WrapEventIcon(icn) // evb, _ := gtk.EventBoxNew()
} // evb.Add(icn)
func WrapEventIcon(icn *Icon) *EventIcon { // return &EventIcon{
icn.Show() // EventBox: evb,
evb, _ := gtk.EventBoxNew() // Icon: icn,
evb.Add(icn) // }
// }
return &EventIcon{
EventBox: evb,
Icon: icn,
}
}
type ToggleButtonImage struct { type ToggleButtonImage struct {
gtk.ToggleButton gtk.ToggleButton
@ -232,8 +217,3 @@ func NewCustomToggleButtonImage(img RoundIconContainer, content text.Rich) *Togg
Box: box, Box: box,
} }
} }
func (t *ToggleButtonImage) Reset() {
t.Labeler.Reset()
t.Image.Reset()
}

View File

@ -10,6 +10,7 @@ import (
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango" "github.com/gotk3/gotk3/pango"
"github.com/pkg/errors"
) )
type Labeler interface { type Labeler interface {
@ -20,13 +21,11 @@ type Labeler interface {
SetLabelUnsafe(text.Rich) SetLabelUnsafe(text.Rich)
GetLabel() text.Rich GetLabel() text.Rich
GetText() string GetText() string
Reset()
} }
// SuperLabeler represents a label that inherits the current labeler. // SuperLabeler represents a label that inherits the current labeler.
type SuperLabeler interface { type SuperLabeler interface {
SetLabelUnsafe(text.Rich) SetLabelUnsafe(text.Rich)
Reset()
} }
type LabelerFn = func(context.Context, cchat.LabelContainer) (func(), error) type LabelerFn = func(context.Context, cchat.LabelContainer) (func(), error)
@ -35,9 +34,6 @@ type Label struct {
gtk.Label gtk.Label
Current text.Rich Current text.Rich
// Reusable primitive.
r *Reusable
// super unexported field for inheritance // super unexported field for inheritance
super SuperLabeler super SuperLabeler
} }
@ -58,11 +54,6 @@ func NewLabel(content text.Rich) *Label {
Current: content, Current: content,
} }
// reusable primitive
l.r = NewReusable(func(nl *nullLabel) {
l.SetLabelUnsafe(nl.Rich)
})
return l return l
} }
@ -80,23 +71,15 @@ func (l *Label) validsuper() bool {
return !ok && l.super != nil 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) { func (l *Label) AsyncSetLabel(fn LabelerFn, info string) {
AsyncUse(l.r, func(ctx context.Context) (interface{}, func(), error) { ctx := primitives.HandleDestroyCtx(context.Background(), l)
nl := &nullLabel{} gts.Async(func() (func(), error) {
f, err := fn(ctx, nl) f, err := fn(ctx, l)
return nl, f, err if err != nil {
return nil, errors.Wrap(err, "failed to load iconer")
}
return func() { l.Connect("destroy", f) }, nil
}) })
} }

View File

@ -1,6 +1,7 @@
package labeluri package labeluri
import ( import (
"context"
"fmt" "fmt"
"html" "html"
"net/url" "net/url"
@ -211,7 +212,7 @@ func popoverImg(url string, round bool) gtk.IWidget {
img.SetHAlign(gtk.ALIGN_CENTER) img.SetHAlign(gtk.ALIGN_CENTER)
img.Show() img.Show()
httputil.AsyncImageSized(idl, url) httputil.AsyncImage(context.Background(), idl, url)
btn.SetHAlign(gtk.ALIGN_CENTER) btn.SetHAlign(gtk.ALIGN_CENTER)
btn.SetRelief(gtk.RELIEF_NONE) btn.SetRelief(gtk.RELIEF_NONE)
@ -268,7 +269,7 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle)
img.Show() img.Show()
// Asynchronously fetch the image. // Asynchronously fetch the image.
httputil.AsyncImageSized(img, uri) httputil.AsyncImage(context.Background(), img, uri)
btn, _ := gtk.ButtonNew() btn, _ := gtk.ButtonNew()
btn.Add(img) btn.Add(img)

View File

@ -20,10 +20,3 @@ type nullLabel struct {
} }
func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t } func (n *nullLabel) SetLabel(t text.Rich) { n.Rich = t }
// used for grabbing url without changing state
type nullIcon struct {
url string
}
func (i *nullIcon) SetIcon(url string) { i.url = url }

View File

@ -22,9 +22,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const IconSize = 48
const IconName = "face-plain-symbolic"
// Servicer extends server.RowController to add session. // Servicer extends server.RowController to add session.
type Servicer interface { type Servicer interface {
// Service asks the controller for its service. // Service asks the controller for its service.
@ -52,8 +49,9 @@ type Servicer interface {
// Row represents a session row entry in the session List. // Row represents a session row entry in the session List.
type Row struct { type Row struct {
*gtk.ListBoxRow *gtk.ListBoxRow
avatar *roundimage.Avatar avatar *roundimage.Avatar
icon *rich.EventIcon // nilable iconBox *gtk.EventBox
icon *rich.Icon // nillable
parentcrumb traverse.Breadcrumber parentcrumb traverse.Breadcrumber
@ -107,6 +105,10 @@ var rowCSS = primitives.PrepareClassCSS("session-row",
0.65 0.65
), 0.85); ), 0.85);
} }
.session-row.failed {
background-color: alpha(red, 0.45);
}
`) `)
var rowIconCSS = primitives.PrepareClassCSS("session-icon", ` var rowIconCSS = primitives.PrepareClassCSS("session-icon", `
@ -114,12 +116,19 @@ var rowIconCSS = primitives.PrepareClassCSS("session-icon", `
padding: 4px; padding: 4px;
margin: 0; 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 { func New(parent traverse.Breadcrumber, ses cchat.Session, ctrl Servicer) *Row {
row := newRow(parent, text.Rich{}, ctrl) row := newRow(parent, text.Rich{}, ctrl)
row.SetSession(ses) 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.SetText(name.Content)
row.avatar.Show() row.avatar.Show()
icon := rich.NewCustomIcon(row.avatar, IconSize) row.iconBox, _ = gtk.EventBoxNew()
icon.Show() row.iconBox.Show()
row.icon = rich.WrapEventIcon(icon)
row.icon.Icon.SetPlaceholderIcon(IconName, IconSize)
row.icon.Show()
rowIconCSS(row.icon.Icon)
row.ListBoxRow, _ = gtk.ListBoxRowNew() row.ListBoxRow, _ = gtk.ListBoxRowNew()
row.ListBoxRow.Show() row.ListBoxRow.Show()
@ -165,7 +169,7 @@ func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Servicer) *Row {
row.ActionsMenu.InsertActionGroup(row) row.ActionsMenu.InsertActionGroup(row)
// Bind right clicks and show a popover menu on such event. // 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) { if gts.EventIsRightClick(ev) {
row.ActionsMenu.Popup(row) row.ActionsMenu.Popup(row)
} }
@ -215,8 +219,13 @@ func (r *Row) Reset() {
r.ActionsMenu.Reset() // wipe menu items r.ActionsMenu.Reset() // wipe menu items
r.ActionsMenu.AddAction("Remove", r.RemoveSession) 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. // Set a lame placeholder icon.
r.icon.Icon.SetPlaceholderIcon("folder-remote-symbolic", IconSize) r.icon.SetPlaceholderIcon("folder-remote-symbolic", IconSize)
r.Session = nil r.Session = nil
r.cmder = nil r.cmder = nil
@ -258,13 +267,14 @@ func (r *Row) SetLoading() {
r.Session = nil r.Session = nil
// Reset the icon. // Reset the icon.
r.icon.Icon.Reset() primitives.RemoveChildren(r.iconBox)
r.icon = nil
// Remove everything from the row, including the icon. // Remove everything from the row, including the icon.
primitives.RemoveChildren(r) primitives.RemoveChildren(r)
// Remove the failed class. // Remove the failed class.
primitives.RemoveClass(r.icon.Icon, "failed") primitives.RemoveClass(r, "failed")
// Add a loading circle. // Add a loading circle.
spin := spinner.New() spin := spinner.New()
@ -287,15 +297,18 @@ func (r *Row) SetFailed(err error) {
r.SetSensitive(true) r.SetSensitive(true)
// Remove everything off the row. // Remove everything off the row.
primitives.RemoveChildren(r) primitives.RemoveChildren(r)
// Add the icon. // Mark the row as failed.
r.Add(r.icon) primitives.AddClass(r, "failed")
// 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")
// SetFailed, but also add the callback to retry. if r.icon == nil {
// r.Row.SetFailed(err, r.ReconnectSession) 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) { 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.Session = ses
r.sessionID = ses.ID() r.sessionID = ses.ID()
r.SetTooltipMarkup(markup.Render(ses.Name())) r.SetTooltipMarkup(markup.Render(ses.Name()))
r.icon.Icon.SetPlaceholderIcon(IconName, IconSize)
r.avatar.SetText(ses.Name().Content) 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 the session has an icon, then use it.
if iconer := ses.AsIconer(); iconer != nil { 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. // Update to indicate that we're done.
primitives.RemoveChildren(r) primitives.RemoveChildren(r)
r.SetSensitive(true) r.SetSensitive(true)
r.Add(r.icon) r.Add(r.iconBox)
// Bind extra menu items before loading. These items won't be clickable // Bind extra menu items before loading. These items won't be clickable
// during loading. // during loading.