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
}
// 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)
}

View File

@ -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) {

View File

@ -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

View File

@ -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.

View File

@ -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")

View File

@ -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.

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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)})
}

View File

@ -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()
}

View File

@ -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
})
}

View File

@ -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)

View File

@ -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 }

View File

@ -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.