Refactored username container; finishing up typing indicator; minor fixes

This commit is contained in:
diamondburned (Forefront) 2020-07-01 12:56:32 -07:00 committed by diamondburned
parent 1caa476a84
commit 68814c2207
11 changed files with 353 additions and 46 deletions

View File

@ -3,7 +3,6 @@ package container
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
@ -47,7 +46,6 @@ type Container interface {
DeleteMessageUnsafe(cchat.MessageDelete)
Reset()
ScrollToBottom()
// AddPresendMessage adds and displays an unsent message.
AddPresendMessage(msg input.PresendMessage) PresendGridMessage
@ -59,6 +57,10 @@ type Container interface {
type Controller interface {
// BindMenu expects the controller to add actioner into the message.
BindMenu(GridMessage)
// Bottomed returns whether or not the message scroller is at the bottom.
Bottomed() bool
// ScrollToBottom scrolls the message view to the bottom.
// ScrollToBottom()
}
// Constructor is an interface for making custom message implementations which
@ -73,8 +75,8 @@ const ColumnSpacing = 10
// GridContainer is an implementation of Container, which allows flexible
// message grids.
type GridContainer struct {
*autoscroll.ScrolledWindow
*GridStore
Controller
}
// gridMessage w/ required internals
@ -86,16 +88,9 @@ type gridMessage struct {
var _ Container = (*GridContainer)(nil)
func NewGridContainer(constr Constructor, ctrl Controller) *GridContainer {
store := NewGridStore(constr, ctrl)
sw := autoscroll.NewScrolledWindow()
sw.Add(store.Grid)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
sw.Show()
return &GridContainer{
ScrolledWindow: sw,
GridStore: store,
GridStore: NewGridStore(constr, ctrl),
Controller: ctrl,
}
}
@ -106,7 +101,7 @@ func (c *GridContainer) CreateMessageUnsafe(msg cchat.MessageCreate) {
c.GridStore.CreateMessageUnsafe(msg)
// Determine if the user is scrolled to the bottom for cleaning up.
if !c.ScrolledWindow.Bottomed {
if !c.Bottomed() {
return
}
@ -136,9 +131,3 @@ func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
gts.ExecAsync(func() { c.DeleteMessageUnsafe(msg) })
}
// Reset is not thread-safe.
func (c *GridContainer) Reset() {
c.GridStore.Reset()
c.ScrolledWindow.Bottomed = true
}

View File

@ -128,7 +128,7 @@ func (c *Container) CreateMessage(msg cchat.MessageCreate) {
// Did the handler wipe old messages? It will only do so if the user is
// scrolled to the bottom.
if !c.ScrolledWindow.Bottomed {
if !c.Bottomed() {
// If we're not at the bottom, then we exit.
return
}

View File

@ -11,7 +11,7 @@ import (
)
type GridStore struct {
Grid *gtk.Grid
*gtk.Grid
Construct Constructor
Controller Controller

View File

@ -4,6 +4,8 @@ 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"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
@ -20,6 +22,12 @@ type InputView struct {
Completer *completion.View
}
var textCSS = primitives.PrepareCSS(`
textview, textview * {
background-color: transparent;
}
`)
func NewView(ctrl Controller) *InputView {
text, _ := gtk.TextViewNew()
text.SetSensitive(false)
@ -30,6 +38,9 @@ func NewView(ctrl Controller) *InputView {
text.SetProperty("bottom-margin", inputmargin)
text.Show()
primitives.AddClass(text, "message-input")
primitives.AttachCSS(text, textCSS)
// Bind the text event handler to text first.
c := completion.New(text)
@ -37,12 +48,7 @@ func NewView(ctrl Controller) *InputView {
f := NewField(text, ctrl)
f.Show()
// // Connect to the field's revealer. On resize, we want the autocompleter to
// // have the right padding too.
// f.username.Connect("size-allocate", func(w gtk.IWidget) {
// // Set the autocompleter's left margin to be the same.
// c.SetMarginStart(w.ToWidget().GetAllocatedWidth())
// })
primitives.AddClass(f, "input-field")
return &InputView{f, c}
}
@ -57,7 +63,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 +79,11 @@ 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.SetVAlign(gtk.ALIGN_END)
username.Show()
buf, _ := text.GetBuffer()
@ -90,8 +97,9 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
box.Show()
field := &Field{
Box: box,
username: username,
Box: box,
Username: username,
// typing: typing,
TextScroll: sw,
text: text,
buffer: buf,
@ -102,6 +110,13 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
text.SetFocusVAdjustment(sw.GetVAdjustment())
text.Connect("key-press-event", field.keyDown)
// // Connect to the field's revealer. On resize, we want the autocompleter to
// // have the right padding too.
// f.username.Connect("size-allocate", func(w gtk.IWidget) {
// // Set the autocompleter's left margin to be the same.
// c.SetMarginStart(w.ToWidget().GetAllocatedWidth())
// })
return field
}
@ -113,7 +128,7 @@ func (f *Field) Reset() {
f.UserID = ""
f.Sender = nil
f.editor = nil
f.username.Reset()
f.Username.Reset()
// reset the input
f.buffer.Delete(f.buffer.GetBounds())
@ -123,7 +138,7 @@ func (f *Field) Reset() {
// disabled. Reset() should be called first.
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
// Update the left username container in the input.
f.username.Update(session, sender)
f.Username.Update(session, sender)
f.UserID = session.ID()
// Set the sender.

View File

@ -58,9 +58,9 @@ func (f *Field) sendInput() {
f.SendMessage(SendMessageData{
time: time.Now().UTC(),
content: text,
author: f.username.GetLabel(),
author: f.Username.GetLabel(),
authorID: f.UserID,
authorURL: f.username.GetIconURL(),
authorURL: f.Username.GetIconURL(),
nonce: f.generateNonce(),
})
}

View File

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

View File

@ -81,7 +81,7 @@ func NewEmptyContainer() *GenericContainer {
ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
ts.SetXAlign(1) // right align
ts.SetVAlign(gtk.ALIGN_END)
ts.SetSelectable(true)
// ts.SetSelectable(true)
ts.Show()
user, _ := gtk.LabelNew("")
@ -90,7 +90,7 @@ func NewEmptyContainer() *GenericContainer {
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
user.SetXAlign(1) // right align
user.SetVAlign(gtk.ALIGN_START)
user.SetSelectable(true)
// user.SetSelectable(true)
user.Show()
content, _ := gtk.LabelNew("")

View File

@ -0,0 +1,56 @@
package typing
import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gtk"
)
var dotsCSS = primitives.PrepareCSS(`
@keyframes breathing {
0% { opacity: 0.66; }
100% { opacity: 0.12; }
}
label {
animation: breathing 800ms infinite alternate;
}
label:nth-child(1) {
animation-delay: 000ms;
}
label:nth-child(2) {
animation-delay: 150ms;
}
label:nth-child(3) {
animation-delay: 300ms;
}
`)
const breathingChar = "●"
func NewDots() *gtk.Box {
c1, _ := gtk.LabelNew(breathingChar)
c1.Show()
c2, _ := gtk.LabelNew(breathingChar)
c2.Show()
c3, _ := gtk.LabelNew(breathingChar)
c3.Show()
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
b.Add(c1)
b.Add(c2)
b.Add(c3)
primitives.AddClass(b, "breathing-dots")
primitives.AttachCSS(c1, dotsCSS)
primitives.AttachCSS(c1, smallfonts)
primitives.AttachCSS(c2, dotsCSS)
primitives.AttachCSS(c2, smallfonts)
primitives.AttachCSS(c3, dotsCSS)
primitives.AttachCSS(c3, smallfonts)
return b
}

View File

@ -0,0 +1,76 @@
package typing
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
)
type State struct {
typers []cchat.Typer
}
func NewState() *State {
return &State{}
}
func (s *State) Empty() bool {
// return len(s.typers) == 0
return false
}
var typingIndicatorCSS = primitives.PrepareCSS(`
.typing-indicator {
border-radius: 8px 8px 0 0;
color: alpha(@theme_fg_color, 0.8);
background-color: @theme_base_color;
}
`)
var smallfonts = primitives.PrepareCSS(`
* { font-size: 0.9em; }
`)
type Container struct {
*gtk.Revealer
empty bool // && state.Empty()
State *State
}
const placeholder = "Bruh moment..."
func New() *Container {
d := NewDots()
d.Show()
l, _ := gtk.LabelNew(placeholder)
l.SetXAlign(0)
l.SetEllipsize(pango.ELLIPSIZE_END)
l.Show()
primitives.AttachCSS(l, smallfonts)
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
b.PackStart(d, false, false, username.VMargin)
b.PackStart(l, true, true, 0)
b.SetMarginStart(username.VMargin * 2)
b.SetMarginEnd(username.VMargin * 2)
b.Show()
r, _ := gtk.RevealerNew()
r.SetTransitionDuration(50)
r.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_CROSSFADE)
r.SetRevealChild(true)
r.Add(b)
state := NewState()
primitives.AddClass(b, "typing-indicator")
primitives.AttachCSS(b, typingIndicatorCSS)
return &Container{
Revealer: r,
State: state,
}
}

View File

@ -13,6 +13,8 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/cozy"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/sadface"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/typing"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
@ -37,7 +39,11 @@ type View struct {
*sadface.FaceView
Box *gtk.Box
Scroller *autoscroll.ScrolledWindow
InputView *input.InputView
MsgBox *gtk.Box
Typing *typing.Container
Container container.Container
contType int // msgIndex
@ -47,16 +53,34 @@ type View struct {
func NewView() *View {
view := &View{}
view.InputView = input.NewView(view)
view.Typing = typing.New()
view.Typing.Show()
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
view.Box.PackEnd(view.InputView, false, false, 0)
view.Box.Show()
view.MsgBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
view.MsgBox.PackEnd(view.Typing, false, false, 0)
view.MsgBox.Show()
// Create the message container, which will use PackEnd to add the widget on
// TOP of the input view.
// TOP of the typing indicator.
view.createMessageContainer()
view.Scroller = autoscroll.NewScrolledWindow()
view.Scroller.Add(view.MsgBox)
view.Scroller.Show()
// A separator to go inbetween.
sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL)
sep.Show()
view.InputView = input.NewView(view)
view.InputView.Show()
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
view.Box.PackStart(view.Scroller, true, true, 0)
view.Box.PackStart(sep, false, false, 0)
view.Box.PackStart(view.InputView, false, false, 0)
view.Box.Show()
// placeholder logo
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256())
logo.Show()
@ -68,7 +92,7 @@ func NewView() *View {
func (v *View) createMessageContainer() {
// Remove the old message container.
if v.Container != nil {
v.Box.Remove(v.Container)
v.MsgBox.Remove(v.Container)
}
// Update the container type.
@ -80,15 +104,20 @@ func (v *View) createMessageContainer() {
}
// Add the new message container.
v.Box.PackEnd(v.Container, true, true, 0)
v.MsgBox.PackEnd(v.Container, true, true, 0)
}
func (v *View) Bottomed() bool { return v.Scroller.Bottomed }
func (v *View) Reset() {
v.state.Reset() // Reset the state variables.
v.FaceView.Reset() // Switch back to the main screen.
v.InputView.Reset() // Reset the input.
v.Container.Reset() // Clean all messages.
// Keep the scroller at the bottom.
v.Scroller.Bottomed = true
// Recreate the message container if the type is different.
if v.contType != msgIndex {
v.createMessageContainer()

View File

@ -48,7 +48,7 @@ func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
input.Connect("key-press-event", KeyDownHandler(l, input.GrabFocus))
ibuf, _ := input.GetBuffer()
ibuf.Connect("changed", func() {
ibuf.Connect("end-user-action", func() {
t, v := State(ibuf)
c.Cursor = v
c.Words, c.Index = split.SpaceIndexed(t, v)