diff --git a/internal/ui/messages/input/input.go b/internal/ui/messages/input/input.go index b6384fb..0703c68 100644 --- a/internal/ui/messages/input/input.go +++ b/internal/ui/messages/input/input.go @@ -4,6 +4,7 @@ import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/log" "github.com/diamondburned/cchat-gtk/internal/ui/messages/input/completion" + "github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput" "github.com/gotk3/gotk3/gtk" "github.com/pkg/errors" @@ -57,7 +58,7 @@ func (v *InputView) SetSender(session cchat.Session, sender cchat.ServerMessageS type Field struct { *gtk.Box - username *usernameContainer + username *username.Container TextScroll *gtk.ScrolledWindow text *gtk.TextView @@ -73,10 +74,10 @@ type Field struct { editingID string // never empty } -const inputmargin = 4 +const inputmargin = username.VMargin func NewField(text *gtk.TextView, ctrl Controller) *Field { - username := newUsernameContainer() + username := username.NewContainer() username.Show() buf, _ := text.GetBuffer() diff --git a/internal/ui/messages/input/username/username.go b/internal/ui/messages/input/username/username.go new file mode 100644 index 0000000..de54849 --- /dev/null +++ b/internal/ui/messages/input/username/username.go @@ -0,0 +1,142 @@ +package username + +import ( + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-gtk/internal/gts" + "github.com/diamondburned/cchat-gtk/internal/ui/config" + "github.com/diamondburned/cchat-gtk/internal/ui/rich" + "github.com/diamondburned/cchat/text" + "github.com/diamondburned/imgutil" + "github.com/gotk3/gotk3/gtk" +) + +const AvatarSize = 24 +const VMargin = 4 + +var showUser = true +var currentRevealer = func(bool) {} // noop by default + +func init() { + // Bind this revealer in settings. + config.AppearanceAdd("Show Username in Input", config.Switch( + &showUser, + func(b bool) { currentRevealer(b) }, + )) +} + +type Container struct { + *gtk.Revealer + main *gtk.Box + avatar *rich.Icon + label *rich.Label +} + +var ( + _ cchat.LabelContainer = (*Container)(nil) + _ cchat.IconContainer = (*Container)(nil) +) + +func NewContainer() *Container { + avatar := rich.NewIcon(AvatarSize, imgutil.Round(true)) + 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.SetMarginStart(10) + box.SetMarginEnd(10) + box.SetMarginTop(VMargin) + box.SetMarginBottom(VMargin) + box.SetVAlign(gtk.ALIGN_START) + box.Show() + + rev, _ := gtk.RevealerNew() + rev.SetRevealChild(false) + rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) + rev.SetTransitionDuration(50) + rev.Add(box) + + // Bind the current global revealer to this revealer for settings. This + // operation should be thread-safe, as everything is being done in the main + // thread. + currentRevealer = rev.SetRevealChild + + return &Container{ + Revealer: rev, + main: box, + avatar: avatar, + label: label, + } +} + +func (u *Container) SetRevealChild(reveal bool) { + // Only reveal if showUser is true. + u.Revealer.SetRevealChild(reveal && showUser) +} + +// shouldReveal returns whether or not the container should reveal. +func (u *Container) shouldReveal() bool { + return !u.label.GetLabel().Empty() && showUser +} + +func (u *Container) Reset() { + u.SetRevealChild(false) + u.avatar.Reset() + u.label.Reset() +} + +// Update is not thread-safe. +func (u *Container) Update(session cchat.Session, sender cchat.ServerMessageSender) { + // Set the fallback username. + u.label.SetLabelUnsafe(session.Name()) + // Reveal the name if it's not empty. + u.SetRevealChild(u.shouldReveal()) + + // Does sender (aka Server) implement ServerNickname? If yes, use it. + if nicknamer, ok := sender.(cchat.ServerNickname); ok { + u.label.AsyncSetLabel(nicknamer.Nickname, "Error fetching server nickname") + } + + // Does session implement an icon? Update if yes. + if iconer, ok := session.(cchat.Icon); ok { + u.avatar.AsyncSetIconer(iconer, "Error fetching session icon URL") + } +} + +// GetLabel is not thread-safe. +func (u *Container) GetLabel() text.Rich { + return u.label.GetLabel() +} + +// SetLabel is thread-safe. +func (u *Container) SetLabel(content text.Rich) { + gts.ExecAsync(func() { + u.label.SetLabelUnsafe(content) + + // Reveal if the name is not empty. + u.SetRevealChild(u.shouldReveal()) + }) +} + +// SetIcon is thread-safe. +func (u *Container) SetIcon(url string) { + gts.ExecAsync(func() { + u.avatar.SetIconUnsafe(url) + + // Reveal if the icon URL is not empty. We don't touch anything if the + // URL is empty, as the name might not be. + if url != "" { + u.SetRevealChild(true) + } + }) +} + +// GetIconURL is not thread-safe. +func (u *Container) GetIconURL() string { + return u.avatar.URL() +}