From 8c5ecd418e5fc29c0394ec575be0bb536411513b Mon Sep 17 00:00:00 2001 From: "diamondburned (Forefront)" Date: Thu, 2 Jul 2020 20:22:48 -0700 Subject: [PATCH] WIP, rebase me --- internal/ui/messages/container/container.go | 27 ++----- internal/ui/messages/container/cozy/cozy.go | 2 +- internal/ui/messages/container/grid.go | 2 +- internal/ui/messages/input/input.go | 36 ++++++--- internal/ui/messages/input/send.go | 4 +- internal/ui/messages/message/message.go | 4 +- internal/ui/messages/typing/dots.go | 56 ++++++++++++++ internal/ui/messages/typing/typing.go | 76 +++++++++++++++++++ internal/ui/messages/view.go | 43 +++++++++-- .../ui/primitives/completion/completer.go | 2 +- 10 files changed, 208 insertions(+), 44 deletions(-) create mode 100644 internal/ui/messages/typing/dots.go create mode 100644 internal/ui/messages/typing/typing.go diff --git a/internal/ui/messages/container/container.go b/internal/ui/messages/container/container.go index ba1a045..280c57a 100644 --- a/internal/ui/messages/container/container.go +++ b/internal/ui/messages/container/container.go @@ -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 -} diff --git a/internal/ui/messages/container/cozy/cozy.go b/internal/ui/messages/container/cozy/cozy.go index 974fdea..686ee74 100644 --- a/internal/ui/messages/container/cozy/cozy.go +++ b/internal/ui/messages/container/cozy/cozy.go @@ -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 } diff --git a/internal/ui/messages/container/grid.go b/internal/ui/messages/container/grid.go index 6499900..0c63be0 100644 --- a/internal/ui/messages/container/grid.go +++ b/internal/ui/messages/container/grid.go @@ -11,7 +11,7 @@ import ( ) type GridStore struct { - Grid *gtk.Grid + *gtk.Grid Construct Constructor Controller Controller diff --git a/internal/ui/messages/input/input.go b/internal/ui/messages/input/input.go index 0703c68..bb0fd8d 100644 --- a/internal/ui/messages/input/input.go +++ b/internal/ui/messages/input/input.go @@ -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. diff --git a/internal/ui/messages/input/send.go b/internal/ui/messages/input/send.go index 5d3e9ec..8ff94c9 100644 --- a/internal/ui/messages/input/send.go +++ b/internal/ui/messages/input/send.go @@ -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(), }) } diff --git a/internal/ui/messages/message/message.go b/internal/ui/messages/message/message.go index 2f450b2..933b742 100644 --- a/internal/ui/messages/message/message.go +++ b/internal/ui/messages/message/message.go @@ -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("") diff --git a/internal/ui/messages/typing/dots.go b/internal/ui/messages/typing/dots.go new file mode 100644 index 0000000..efda33b --- /dev/null +++ b/internal/ui/messages/typing/dots.go @@ -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 +} diff --git a/internal/ui/messages/typing/typing.go b/internal/ui/messages/typing/typing.go new file mode 100644 index 0000000..3427af4 --- /dev/null +++ b/internal/ui/messages/typing/typing.go @@ -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, + } +} diff --git a/internal/ui/messages/view.go b/internal/ui/messages/view.go index 91beb22..99d4719 100644 --- a/internal/ui/messages/view.go +++ b/internal/ui/messages/view.go @@ -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() diff --git a/internal/ui/primitives/completion/completer.go b/internal/ui/primitives/completion/completer.go index ac83e93..6cfaff5 100644 --- a/internal/ui/primitives/completion/completer.go +++ b/internal/ui/primitives/completion/completer.go @@ -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)