fixed scaling with image API refactor, minor tweaks
This commit is contained in:
parent
296346bbe7
commit
d3048fe08c
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)})
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue