mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-12-02 09:57:11 +00:00
Refactored username container; finishing up typing indicator; minor fixes
This commit is contained in:
parent
1caa476a84
commit
68814c2207
|
|
@ -3,7 +3,6 @@ package container
|
||||||
import (
|
import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"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/input"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
|
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
|
||||||
|
|
@ -47,7 +46,6 @@ type Container interface {
|
||||||
DeleteMessageUnsafe(cchat.MessageDelete)
|
DeleteMessageUnsafe(cchat.MessageDelete)
|
||||||
|
|
||||||
Reset()
|
Reset()
|
||||||
ScrollToBottom()
|
|
||||||
|
|
||||||
// AddPresendMessage adds and displays an unsent message.
|
// AddPresendMessage adds and displays an unsent message.
|
||||||
AddPresendMessage(msg input.PresendMessage) PresendGridMessage
|
AddPresendMessage(msg input.PresendMessage) PresendGridMessage
|
||||||
|
|
@ -59,6 +57,10 @@ type Container interface {
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
// BindMenu expects the controller to add actioner into the message.
|
// BindMenu expects the controller to add actioner into the message.
|
||||||
BindMenu(GridMessage)
|
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
|
// 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
|
// GridContainer is an implementation of Container, which allows flexible
|
||||||
// message grids.
|
// message grids.
|
||||||
type GridContainer struct {
|
type GridContainer struct {
|
||||||
*autoscroll.ScrolledWindow
|
|
||||||
*GridStore
|
*GridStore
|
||||||
|
Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// gridMessage w/ required internals
|
// gridMessage w/ required internals
|
||||||
|
|
@ -86,16 +88,9 @@ type gridMessage struct {
|
||||||
var _ Container = (*GridContainer)(nil)
|
var _ Container = (*GridContainer)(nil)
|
||||||
|
|
||||||
func NewGridContainer(constr Constructor, ctrl Controller) *GridContainer {
|
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{
|
return &GridContainer{
|
||||||
ScrolledWindow: sw,
|
GridStore: NewGridStore(constr, ctrl),
|
||||||
GridStore: store,
|
Controller: ctrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +101,7 @@ func (c *GridContainer) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||||
c.GridStore.CreateMessageUnsafe(msg)
|
c.GridStore.CreateMessageUnsafe(msg)
|
||||||
|
|
||||||
// Determine if the user is scrolled to the bottom for cleaning up.
|
// Determine if the user is scrolled to the bottom for cleaning up.
|
||||||
if !c.ScrolledWindow.Bottomed {
|
if !c.Bottomed() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,9 +131,3 @@ func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
|
||||||
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
|
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
|
||||||
gts.ExecAsync(func() { c.DeleteMessageUnsafe(msg) })
|
gts.ExecAsync(func() { c.DeleteMessageUnsafe(msg) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset is not thread-safe.
|
|
||||||
func (c *GridContainer) Reset() {
|
|
||||||
c.GridStore.Reset()
|
|
||||||
c.ScrolledWindow.Bottomed = true
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Did the handler wipe old messages? It will only do so if the user is
|
||||||
// scrolled to the bottom.
|
// scrolled to the bottom.
|
||||||
if !c.ScrolledWindow.Bottomed {
|
if !c.Bottomed() {
|
||||||
// If we're not at the bottom, then we exit.
|
// If we're not at the bottom, then we exit.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type GridStore struct {
|
type GridStore struct {
|
||||||
Grid *gtk.Grid
|
*gtk.Grid
|
||||||
|
|
||||||
Construct Constructor
|
Construct Constructor
|
||||||
Controller Controller
|
Controller Controller
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"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/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/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
@ -20,6 +22,12 @@ type InputView struct {
|
||||||
Completer *completion.View
|
Completer *completion.View
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var textCSS = primitives.PrepareCSS(`
|
||||||
|
textview, textview * {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
func NewView(ctrl Controller) *InputView {
|
func NewView(ctrl Controller) *InputView {
|
||||||
text, _ := gtk.TextViewNew()
|
text, _ := gtk.TextViewNew()
|
||||||
text.SetSensitive(false)
|
text.SetSensitive(false)
|
||||||
|
|
@ -30,6 +38,9 @@ func NewView(ctrl Controller) *InputView {
|
||||||
text.SetProperty("bottom-margin", inputmargin)
|
text.SetProperty("bottom-margin", inputmargin)
|
||||||
text.Show()
|
text.Show()
|
||||||
|
|
||||||
|
primitives.AddClass(text, "message-input")
|
||||||
|
primitives.AttachCSS(text, textCSS)
|
||||||
|
|
||||||
// Bind the text event handler to text first.
|
// Bind the text event handler to text first.
|
||||||
c := completion.New(text)
|
c := completion.New(text)
|
||||||
|
|
||||||
|
|
@ -37,12 +48,7 @@ func NewView(ctrl Controller) *InputView {
|
||||||
f := NewField(text, ctrl)
|
f := NewField(text, ctrl)
|
||||||
f.Show()
|
f.Show()
|
||||||
|
|
||||||
// // Connect to the field's revealer. On resize, we want the autocompleter to
|
primitives.AddClass(f, "input-field")
|
||||||
// // 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 &InputView{f, c}
|
return &InputView{f, c}
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +63,7 @@ func (v *InputView) SetSender(session cchat.Session, sender cchat.ServerMessageS
|
||||||
|
|
||||||
type Field struct {
|
type Field struct {
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
username *usernameContainer
|
Username *username.Container
|
||||||
|
|
||||||
TextScroll *gtk.ScrolledWindow
|
TextScroll *gtk.ScrolledWindow
|
||||||
text *gtk.TextView
|
text *gtk.TextView
|
||||||
|
|
@ -73,10 +79,11 @@ type Field struct {
|
||||||
editingID string // never empty
|
editingID string // never empty
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputmargin = 4
|
const inputmargin = username.VMargin
|
||||||
|
|
||||||
func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||||
username := newUsernameContainer()
|
username := username.NewContainer()
|
||||||
|
username.SetVAlign(gtk.ALIGN_END)
|
||||||
username.Show()
|
username.Show()
|
||||||
|
|
||||||
buf, _ := text.GetBuffer()
|
buf, _ := text.GetBuffer()
|
||||||
|
|
@ -90,8 +97,9 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||||
box.Show()
|
box.Show()
|
||||||
|
|
||||||
field := &Field{
|
field := &Field{
|
||||||
Box: box,
|
Box: box,
|
||||||
username: username,
|
Username: username,
|
||||||
|
// typing: typing,
|
||||||
TextScroll: sw,
|
TextScroll: sw,
|
||||||
text: text,
|
text: text,
|
||||||
buffer: buf,
|
buffer: buf,
|
||||||
|
|
@ -102,6 +110,13 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||||
text.SetFocusVAdjustment(sw.GetVAdjustment())
|
text.SetFocusVAdjustment(sw.GetVAdjustment())
|
||||||
text.Connect("key-press-event", field.keyDown)
|
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
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,7 +128,7 @@ func (f *Field) Reset() {
|
||||||
f.UserID = ""
|
f.UserID = ""
|
||||||
f.Sender = nil
|
f.Sender = nil
|
||||||
f.editor = nil
|
f.editor = nil
|
||||||
f.username.Reset()
|
f.Username.Reset()
|
||||||
|
|
||||||
// reset the input
|
// reset the input
|
||||||
f.buffer.Delete(f.buffer.GetBounds())
|
f.buffer.Delete(f.buffer.GetBounds())
|
||||||
|
|
@ -123,7 +138,7 @@ func (f *Field) Reset() {
|
||||||
// disabled. Reset() should be called first.
|
// disabled. Reset() should be called first.
|
||||||
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
||||||
// Update the left username container in the input.
|
// Update the left username container in the input.
|
||||||
f.username.Update(session, sender)
|
f.Username.Update(session, sender)
|
||||||
f.UserID = session.ID()
|
f.UserID = session.ID()
|
||||||
|
|
||||||
// Set the sender.
|
// Set the sender.
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,9 @@ func (f *Field) sendInput() {
|
||||||
f.SendMessage(SendMessageData{
|
f.SendMessage(SendMessageData{
|
||||||
time: time.Now().UTC(),
|
time: time.Now().UTC(),
|
||||||
content: text,
|
content: text,
|
||||||
author: f.username.GetLabel(),
|
author: f.Username.GetLabel(),
|
||||||
authorID: f.UserID,
|
authorID: f.UserID,
|
||||||
authorURL: f.username.GetIconURL(),
|
authorURL: f.Username.GetIconURL(),
|
||||||
nonce: f.generateNonce(),
|
nonce: f.generateNonce(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
142
internal/ui/messages/input/username/username.go
Normal file
142
internal/ui/messages/input/username/username.go
Normal 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()
|
||||||
|
}
|
||||||
|
|
@ -81,7 +81,7 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
ts.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
||||||
ts.SetXAlign(1) // right align
|
ts.SetXAlign(1) // right align
|
||||||
ts.SetVAlign(gtk.ALIGN_END)
|
ts.SetVAlign(gtk.ALIGN_END)
|
||||||
ts.SetSelectable(true)
|
// ts.SetSelectable(true)
|
||||||
ts.Show()
|
ts.Show()
|
||||||
|
|
||||||
user, _ := gtk.LabelNew("")
|
user, _ := gtk.LabelNew("")
|
||||||
|
|
@ -90,7 +90,7 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||||
user.SetXAlign(1) // right align
|
user.SetXAlign(1) // right align
|
||||||
user.SetVAlign(gtk.ALIGN_START)
|
user.SetVAlign(gtk.ALIGN_START)
|
||||||
user.SetSelectable(true)
|
// user.SetSelectable(true)
|
||||||
user.Show()
|
user.Show()
|
||||||
|
|
||||||
content, _ := gtk.LabelNew("")
|
content, _ := gtk.LabelNew("")
|
||||||
|
|
|
||||||
56
internal/ui/messages/typing/dots.go
Normal file
56
internal/ui/messages/typing/dots.go
Normal 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
|
||||||
|
}
|
||||||
76
internal/ui/messages/typing/typing.go
Normal file
76
internal/ui/messages/typing/typing.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,8 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/cozy"
|
"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/input"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/sadface"
|
"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/diamondburned/cchat-gtk/internal/ui/service/menu"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
@ -37,7 +39,11 @@ type View struct {
|
||||||
*sadface.FaceView
|
*sadface.FaceView
|
||||||
Box *gtk.Box
|
Box *gtk.Box
|
||||||
|
|
||||||
|
Scroller *autoscroll.ScrolledWindow
|
||||||
InputView *input.InputView
|
InputView *input.InputView
|
||||||
|
|
||||||
|
MsgBox *gtk.Box
|
||||||
|
Typing *typing.Container
|
||||||
Container container.Container
|
Container container.Container
|
||||||
contType int // msgIndex
|
contType int // msgIndex
|
||||||
|
|
||||||
|
|
@ -47,16 +53,34 @@ type View struct {
|
||||||
|
|
||||||
func NewView() *View {
|
func NewView() *View {
|
||||||
view := &View{}
|
view := &View{}
|
||||||
view.InputView = input.NewView(view)
|
view.Typing = typing.New()
|
||||||
|
view.Typing.Show()
|
||||||
|
|
||||||
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
view.MsgBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
||||||
view.Box.PackEnd(view.InputView, false, false, 0)
|
view.MsgBox.PackEnd(view.Typing, false, false, 0)
|
||||||
view.Box.Show()
|
view.MsgBox.Show()
|
||||||
|
|
||||||
// Create the message container, which will use PackEnd to add the widget on
|
// 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.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
|
// placeholder logo
|
||||||
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256())
|
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256())
|
||||||
logo.Show()
|
logo.Show()
|
||||||
|
|
@ -68,7 +92,7 @@ func NewView() *View {
|
||||||
func (v *View) createMessageContainer() {
|
func (v *View) createMessageContainer() {
|
||||||
// Remove the old message container.
|
// Remove the old message container.
|
||||||
if v.Container != nil {
|
if v.Container != nil {
|
||||||
v.Box.Remove(v.Container)
|
v.MsgBox.Remove(v.Container)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the container type.
|
// Update the container type.
|
||||||
|
|
@ -80,15 +104,20 @@ func (v *View) createMessageContainer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new message container.
|
// 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() {
|
func (v *View) Reset() {
|
||||||
v.state.Reset() // Reset the state variables.
|
v.state.Reset() // Reset the state variables.
|
||||||
v.FaceView.Reset() // Switch back to the main screen.
|
v.FaceView.Reset() // Switch back to the main screen.
|
||||||
v.InputView.Reset() // Reset the input.
|
v.InputView.Reset() // Reset the input.
|
||||||
v.Container.Reset() // Clean all messages.
|
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.
|
// Recreate the message container if the type is different.
|
||||||
if v.contType != msgIndex {
|
if v.contType != msgIndex {
|
||||||
v.createMessageContainer()
|
v.createMessageContainer()
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
|
||||||
input.Connect("key-press-event", KeyDownHandler(l, input.GrabFocus))
|
input.Connect("key-press-event", KeyDownHandler(l, input.GrabFocus))
|
||||||
|
|
||||||
ibuf, _ := input.GetBuffer()
|
ibuf, _ := input.GetBuffer()
|
||||||
ibuf.Connect("changed", func() {
|
ibuf.Connect("end-user-action", func() {
|
||||||
t, v := State(ibuf)
|
t, v := State(ibuf)
|
||||||
c.Cursor = v
|
c.Cursor = v
|
||||||
c.Words, c.Index = split.SpaceIndexed(t, v)
|
c.Words, c.Index = split.SpaceIndexed(t, v)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue