WIP, rebase me

This commit is contained in:
diamondburned (Forefront) 2020-07-02 20:22:48 -07:00
parent 0200da67e0
commit 8c5ecd418e
10 changed files with 208 additions and 44 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

@ -5,6 +5,7 @@ import (
"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"
@ -21,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)
@ -31,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)
@ -38,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}
}
@ -58,7 +63,7 @@ func (v *InputView) SetSender(session cchat.Session, sender cchat.ServerMessageS
type Field struct {
*gtk.Box
username *username.Container
Username *username.Container
TextScroll *gtk.ScrolledWindow
text *gtk.TextView
@ -78,6 +83,7 @@ const inputmargin = username.VMargin
func NewField(text *gtk.TextView, ctrl Controller) *Field {
username := username.NewContainer()
username.SetVAlign(gtk.ALIGN_END)
username.Show()
buf, _ := text.GetBuffer()
@ -91,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,
@ -103,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
}
@ -114,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())
@ -124,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

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