1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2024-12-22 20:27:07 +00:00

Added colors and stuff

This commit is contained in:
diamondburned (Forefront) 2020-06-05 17:47:28 -07:00
parent b852498ee4
commit 0171ac6b52
21 changed files with 590 additions and 251 deletions

5
go.mod
View file

@ -6,12 +6,11 @@ replace github.com/diamondburned/cchat-mock => ../cchat-mock/
require (
github.com/Xuanwo/go-locale v0.2.0
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/diamondburned/cchat v0.0.15
github.com/diamondburned/cchat-mock v0.0.0-20200604043646-de5384bd320d
github.com/diamondburned/cchat-mock v0.0.0-20200605224934-31a53c555ea2
github.com/goodsign/monday v1.0.0
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194
github.com/markbates/pkger v0.17.0
github.com/pkg/errors v0.9.1
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717
golang.org/x/tools v0.0.0-20200529172331-a64b76657301 // indirect
)

36
go.sum
View file

@ -7,16 +7,12 @@ github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3E
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diamondburned/cchat v0.0.10 h1:aiUVgGre5E/HV+Iw6tmBVbuGctQI+JndV9nIDoYuRPY=
github.com/diamondburned/cchat v0.0.10/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.13 h1:p8SyFjiRVCTjvwSJ4FsICGVYVZ3g0Iu02FrwmLuKiKE=
github.com/diamondburned/cchat v0.0.13/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.15 h1:1o4OX8zw/CdSv3Idaylz7vjHVOZKEi/xkg8BpEvtsHY=
github.com/diamondburned/cchat v0.0.15/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat-mock v0.0.0-20200529184140-47fa2491d2fc h1:xYSN3re1QOd5af5zG15pLKfBM+fergw7Rg62UHmE22g=
github.com/diamondburned/cchat-mock v0.0.0-20200529184140-47fa2491d2fc/go.mod h1:rQm5EKhNyBRYHKtirSkf+Db23nr3mTs2bnOThfTfzec=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc=
@ -27,6 +23,13 @@ github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194 h1:bB6XWpxMt2isCWqzj
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/markbates/pkger v0.17.0 h1:RFfyBPufP2V6cddUyyEVSHBpaAnM1WzaMNyqomeT+iY=
github.com/markbates/pkger v0.17.0/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -39,35 +42,22 @@ github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717 h1:3M/uUZajYn/082wzUajekePxpUAZhMTfXvI9R+26SJ0=
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200529172331-a64b76657301 h1:G6CNEgFU8/XwexSnuFw+Jq/WePjRitgy6ofBcPnAIPo=
golang.org/x/tools v0.0.0-20200529172331-a64b76657301/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

64
internal/gts/css.go Normal file
View file

@ -0,0 +1,64 @@
package gts
import (
"bytes"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk"
"github.com/markbates/pkger"
"github.com/pkg/errors"
)
var cssRepos = map[string]*gtk.CssProvider{}
func getDefaultScreen() *gdk.Screen {
d, _ := gdk.DisplayGetDefault()
s, _ := d.GetDefaultScreen()
return s
}
func loadProviders(screen *gdk.Screen) {
for file, repo := range cssRepos {
gtk.AddProviderForScreen(
screen, repo,
uint(gtk.STYLE_PROVIDER_PRIORITY_APPLICATION),
)
// mark as done
delete(cssRepos, file)
}
}
func LoadCSS(files ...string) {
var buf bytes.Buffer
for _, file := range files {
buf.Reset()
if err := readFile(&buf, file); err != nil {
log.Error(errors.Wrap(err, "Failed to load a CSS file"))
continue
}
prov, _ := gtk.CssProviderNew()
if err := prov.LoadFromData(buf.String()); err != nil {
log.Error(errors.Wrap(err, "Failed to parse CSS "+file))
continue
}
cssRepos[file] = prov
}
}
func readFile(buf *bytes.Buffer, file string) error {
f, err := pkger.Open(file)
if err != nil {
return errors.Wrap(err, "Failed to load a CSS file")
}
defer f.Close()
if _, err := buf.ReadFrom(f); err != nil {
return errors.Wrap(err, "Failed to read file")
}
return nil
}

View file

@ -9,6 +9,8 @@ import (
"github.com/gotk3/gotk3/gtk"
)
const AppID = "com.github.diamondburned.cchat-gtk"
var Args = append([]string{}, os.Args...)
var recvPool *sync.Pool
@ -27,7 +29,7 @@ func init() {
},
}
App.Application, _ = gtk.ApplicationNew("com.github.diamondburned.cchat-gtk", 0)
App.Application, _ = gtk.ApplicationNew(AppID, 0)
}
type Windower interface {
@ -47,10 +49,16 @@ type WindowHeaderer interface {
func Main(wfn func() WindowHeaderer) {
App.Application.Connect("activate", func() {
// Load all CSS onto the default screen.
loadProviders(getDefaultScreen())
App.Header, _ = gtk.HeaderBarNew()
App.Header.SetShowCloseButton(true)
App.Header.Show()
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
App.Header.SetCustomTitle(b)
App.Window, _ = gtk.ApplicationWindowNew(App.Application)
App.Window.SetDefaultSize(1000, 500)
App.Window.SetTitlebar(App.Header)

View file

@ -2,6 +2,7 @@ package log
import (
"fmt"
"log"
"os"
"sync"
"time"
@ -59,3 +60,7 @@ func WriteEntry(entry Entry) {
}
}()
}
func Println(v ...interface{}) {
log.Println(v...)
}

View file

@ -1,14 +1,73 @@
package ui
import "github.com/gotk3/gotk3/gtk"
import (
"html"
"strings"
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
"github.com/gotk3/gotk3/gtk"
)
type header struct {
*gtk.Box
left *gtk.Box // TODO
right *headerRight
}
func newHeader() *header {
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
left, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
left.SetSizeRequest(LeftWidth, -1)
left.Show()
right := newHeaderRight()
right.Show()
separator, _ := gtk.SeparatorNew(gtk.ORIENTATION_VERTICAL)
separator.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(left, false, false, 0)
box.PackStart(separator, false, false, 0)
box.PackStart(right, true, true, 0)
box.Show()
// TODO
return &header{box}
return &header{
box,
left,
right,
}
}
const BreadcrumbSlash = `<span weight="light" rise="-1024" size="x-large">/</span>`
func (h *header) SetBreadcrumb(b breadcrumb.Breadcrumb) {
for i := range b {
b[i] = html.EscapeString(b[i])
}
h.right.breadcrumb.SetMarkup(
BreadcrumbSlash + " " + strings.Join(b, " "+BreadcrumbSlash+" "),
)
}
type headerRight struct {
*gtk.Box
breadcrumb *gtk.Label
}
func newHeaderRight() *headerRight {
bc, _ := gtk.LabelNew(BreadcrumbSlash)
bc.SetUseMarkup(true)
bc.SetXAlign(0.0)
bc.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(bc, true, true, 14)
box.Show()
return &headerRight{
Box: box,
breadcrumb: bc,
}
}

View file

@ -5,6 +5,7 @@ import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/humanize"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
@ -104,10 +105,9 @@ func (m *Message) UpdateAuthor(author cchat.MessageAuthor) {
}
func (m *Message) updateAuthorName(name text.Rich) {
m.Username.SetLabel(name.Content)
m.Username.SetTooltipText(name.Content)
m.Username.SetMarkup(parser.RenderMarkup(name))
}
func (m *Message) UpdateContent(content text.Rich) {
m.Content.SetLabel(content.Content)
m.Content.SetMarkup(parser.RenderMarkup(content))
}

View file

@ -0,0 +1 @@
package cozy

View file

@ -10,11 +10,48 @@ import (
"github.com/pkg/errors"
)
type usernameContainer struct {
*gtk.Revealer
label *rich.Label
}
func newUsernameContainer() *usernameContainer {
label := rich.NewLabel(text.Rich{})
label.SetMaxWidthChars(35)
label.SetVAlign(gtk.ALIGN_START)
label.SetMarginTop(inputmargin)
label.SetMarginBottom(inputmargin)
label.SetMarginStart(10)
label.SetMarginEnd(10)
label.Show()
rev, _ := gtk.RevealerNew()
rev.SetRevealChild(false)
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
rev.SetTransitionDuration(50)
rev.Add(label)
return &usernameContainer{rev, label}
}
// GetLabel is not thread-safe.
func (u *usernameContainer) GetLabel() text.Rich {
return u.label.GetLabel()
}
// SetLabel is thread-safe.
func (u *usernameContainer) SetLabel(content text.Rich) {
gts.ExecAsync(func() {
u.label.SetLabelUnsafe(content)
// Reveal if the name is not empty.
u.SetRevealChild(!u.label.GetLabel().Empty())
})
}
type Field struct {
*gtk.Box
namerev *gtk.Revealer
username *rich.Label // TODO
username *usernameContainer
TextScroll *gtk.ScrolledWindow
text *gtk.TextView
@ -30,25 +67,12 @@ type Controller interface {
PresendMessage(msg PresendMessage) (onErr func(error))
}
const inputmargin = 3
const inputmargin = 4
func NewField(ctrl Controller) *Field {
username := rich.NewLabel(text.Rich{})
username.SetMaxWidthChars(35)
username.SetVAlign(gtk.ALIGN_START)
username.SetMarginTop(inputmargin)
username.SetMarginBottom(inputmargin)
username.SetMarginStart(10)
username.SetMarginEnd(10)
username := newUsernameContainer()
username.Show()
namerev, _ := gtk.RevealerNew()
namerev.SetRevealChild(false)
namerev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
namerev.SetTransitionDuration(50)
namerev.Add(username)
namerev.Show()
text, _ := gtk.TextViewNew()
text.SetSensitive(false)
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
@ -68,13 +92,12 @@ func NewField(ctrl Controller) *Field {
sw.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(namerev, false, false, 0)
box.PackStart(username, false, false, 0)
box.PackStart(sw, true, true, 0)
box.Show()
field := &Field{
Box: box,
namerev: namerev,
username: username,
TextScroll: sw,
text: text,
@ -107,9 +130,6 @@ func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSende
log.Warn(err)
}
// Reveal if the name is not empty.
f.namerev.SetRevealChild(!f.username.GetLabel().Empty())
// Set the sender.
f.sender = sender
f.text.SetSensitive(sender != nil) // grey if sender is nil

View file

@ -1 +1,83 @@
package parser
import (
"bytes"
"fmt"
"html"
"sort"
"github.com/diamondburned/cchat/text"
)
type attrAppendMap struct {
appended map[int]string
indices []int
}
func newAttrAppendedMap() attrAppendMap {
return attrAppendMap{
appended: make(map[int]string),
indices: []int{},
}
}
func (a *attrAppendMap) add(ind int, attr string) {
if _, ok := a.appended[ind]; ok {
a.appended[ind] += attr
return
}
a.appended[ind] = attr
a.indices = append(a.indices, ind)
}
func (a attrAppendMap) get(ind int) string {
return a.appended[ind]
}
func (a *attrAppendMap) finalize(strlen int) []int {
// make sure there's always a closing tag at the end so the entire string
// gets flushed.
a.add(strlen, "")
sort.Ints(a.indices)
return a.indices
}
func RenderMarkup(content text.Rich) string {
buf := bytes.Buffer{}
buf.Grow(len(content.Content))
// // Sort so that all starting points are sorted incrementally.
// sort.Slice(content.Segments, func(i, j int) bool {
// i, _ = content.Segments[i].Bounds()
// j, _ = content.Segments[j].Bounds()
// return i < j
// })
// map to append strings to indices
var appended = newAttrAppendedMap()
// Parse all segments.
for _, segment := range content.Segments {
start, end := segment.Bounds()
switch segment := segment.(type) {
case text.Colorer:
appended.add(start, fmt.Sprintf("<span color=\"#%06X\">", segment.Color()))
appended.add(end, "</span>")
}
}
var lastIndex = 0
for _, index := range appended.finalize(len(content.Content)) {
// Write the content.
buf.WriteString(html.EscapeString(content.Content[lastIndex:index]))
// Write the tags.
buf.WriteString(appended.get(index))
// Set the last index.
lastIndex = index
}
return buf.String()
}

View file

@ -0,0 +1,55 @@
package parser
import (
"strings"
"testing"
"github.com/diamondburned/cchat-mock/segments"
"github.com/diamondburned/cchat/text"
)
func TestRenderMarkup(t *testing.T) {
content := text.Rich{Content: "astolfo is the best trap"}
content.Segments = []text.Segment{
segments.NewColored(content.Content, 0x55CDFC),
}
expect := `<span color="#55CDFC">` + content.Content + "</span>"
if text := RenderMarkup(content); text != expect {
t.Fatal("Unexpected text:", text)
}
}
func TestRenderMarkupPartial(t *testing.T) {
content := text.Rich{Content: "random placeholder text go brrr"}
content.Segments = []text.Segment{
// This is absolutely jankery that should not work at all, but we'll try
// it anyway.
coloredSegment{0, 4, 0x55CDFC},
coloredSegment{2, 6, 0xFFFFFF}, // naive parsing, so spans close unexpectedly.
coloredSegment{4, 6, 0xF7A8B8},
}
const expect = "" +
`<span color="#55CDFC">ra<span color="#FFFFFF">nd</span>` +
`<span color="#F7A8B8">om</span></span>`
if text := RenderMarkup(content); !strings.HasPrefix(text, expect) {
t.Fatal("Unexpected text:", text)
}
}
type coloredSegment struct {
start int
end int
color uint32
}
var _ text.Colorer = (*coloredSegment)(nil)
func (c coloredSegment) Bounds() (start, end int) {
return c.start, c.end
}
func (c coloredSegment) Color() uint32 {
return c.color
}

View file

@ -4,6 +4,7 @@ import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
)
@ -13,6 +14,7 @@ import (
type Labeler interface {
cchat.LabelContainer // thread-safe
GetLabel() text.Rich // not thread-safe
GetText() string
}
type Label struct {
@ -26,7 +28,8 @@ var (
)
func NewLabel(content text.Rich) *Label {
label, _ := gtk.LabelNew(content.Content)
label, _ := gtk.LabelNew("")
label.SetMarkup(parser.RenderMarkup(content))
label.SetHAlign(gtk.ALIGN_START)
return &Label{*label, content}
}
@ -34,16 +37,27 @@ func NewLabel(content text.Rich) *Label {
// SetLabel is thread-safe.
func (l *Label) SetLabel(content text.Rich) {
gts.ExecAsync(func() {
l.current = content
l.SetText(content.Content)
l.SetLabelUnsafe(content)
})
}
// SetLabelUnsafe sets the label in the current thread, meaning it's not
// thread-safe.
func (l *Label) SetLabelUnsafe(content text.Rich) {
l.current = content
l.SetMarkup(parser.RenderMarkup(content))
}
// GetLabel is NOT thread-safe.
func (l *Label) GetLabel() text.Rich {
return l.current
}
// GetText is NOT thread-safe.
func (l *Label) GetText() string {
return l.current.Content
}
type ToggleButton struct {
gtk.ToggleButton
Label

View file

@ -0,0 +1,21 @@
package breadcrumb
import "strings"
type Breadcrumb []string
func (b Breadcrumb) String() string {
return strings.Join([]string(b), "/")
}
type Breadcrumber interface {
Breadcrumb() Breadcrumb
}
// Try accepts a nilable breadcrumber and handles it appropriately.
func Try(i Breadcrumber, appended ...string) []string {
if i == nil {
return appended
}
return append(i.Breadcrumb(), appended...)
}

View file

@ -4,6 +4,7 @@ import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
@ -91,7 +92,7 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
}
func (c *Container) AddSession(ses cchat.Session) {
srow := session.New(ses, c.rowctrl)
srow := session.New(c, ses, c.rowctrl)
c.children.addSessionRow(srow)
}
@ -103,6 +104,10 @@ func (c *Container) Sessions() []cchat.Session {
return sessions
}
func (c *Container) Breadcrumb() breadcrumb.Breadcrumb {
return breadcrumb.Try(nil, c.header.reveal.GetText())
}
type header struct {
*gtk.Box
reveal *rich.ToggleButtonImage // no rich text here but it's left aligned

View file

@ -0,0 +1,177 @@
package server
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const ChildrenMargin = 24
type Controller interface {
MessageRowSelected(*Row, cchat.ServerMessage)
}
type Row struct {
*gtk.Box
Button *rich.ToggleButtonImage
Server cchat.Server
Parent breadcrumb.Breadcrumber
ctrl Controller
// enum 1
message cchat.ServerMessage
// enum 2
children *Children
}
func NewRow(parent breadcrumb.Breadcrumber, server cchat.Server, ctrl Controller) *Row {
button := rich.NewToggleButtonImage(text.Rich{}, "")
button.Box.SetHAlign(gtk.ALIGN_START)
button.SetRelief(gtk.RELIEF_NONE)
button.Show()
if err := server.Name(button); err != nil {
log.Error(errors.Wrap(err, "Failed to get the server name"))
button.SetLabel(text.Rich{Content: "Unknown"})
}
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.PackStart(button, false, false, 0)
box.Show()
primitives.AddClass(box, "server")
// TODO: images
var row = &Row{
Box: box,
Button: button,
Server: server,
Parent: parent,
ctrl: ctrl,
}
switch server := server.(type) {
case cchat.ServerList:
row.children = NewChildren(row, server, ctrl)
box.PackStart(row.children, false, false, 0)
primitives.AddClass(box, "server-list")
case cchat.ServerMessage:
row.message = server
primitives.AddClass(box, "server-message")
}
button.Connect("clicked", row.onClick)
return row
}
func (row *Row) GetActive() bool {
return row.Button.GetActive()
}
func (row *Row) onClick() {
switch {
// If the server is a message server. We're only selected if the button is
// pressed.
case row.message != nil && row.GetActive():
row.ctrl.MessageRowSelected(row, row.message)
// If the server is a list of smaller servers.
case row.children != nil:
row.children.SetRevealChild(!row.children.GetRevealChild())
}
}
func (r *Row) Breadcrumb() breadcrumb.Breadcrumb {
return breadcrumb.Try(r.Parent, r.Button.GetText())
}
// Children is a children server with a reference to the parent.
type Children struct {
*gtk.Revealer
Main *gtk.Box
List cchat.ServerList
rowctrl Controller
Rows []*Row
Parent breadcrumb.Breadcrumber
}
func NewChildren(parent breadcrumb.Breadcrumber, list cchat.ServerList, ctrl Controller) *Children {
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
main.SetMarginStart(ChildrenMargin)
main.Show()
rev, _ := gtk.RevealerNew()
rev.SetRevealChild(false)
rev.Add(main)
rev.Show()
children := &Children{
Revealer: rev,
Main: main,
List: list,
rowctrl: ctrl,
Parent: parent,
}
if err := list.Servers(children); err != nil {
log.Error(errors.Wrap(err, "Failed to get servers"))
}
return children
}
func (c *Children) SetServers(servers []cchat.Server) {
gts.ExecAsync(func() {
// Save the current state.
var oldID string
for _, row := range c.Rows {
if row.GetActive() {
oldID = row.Server.ID()
break
}
}
// Update the server list.
for _, row := range c.Rows {
c.Main.Remove(row)
}
c.Rows = make([]*Row, len(servers))
for i, server := range servers {
row := NewRow(c, server, c.rowctrl)
c.Rows[i] = row
c.Main.Add(row)
}
// Update parent reference? Only if it's activated.
if oldID != "" {
for _, row := range c.Rows {
if row.Server.ID() == oldID {
row.Button.SetActive(true)
}
}
}
})
}
func (c *Children) Breadcrumb() breadcrumb.Breadcrumb {
return breadcrumb.Try(c.Parent)
}

View file

@ -1,87 +0,0 @@
package server
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const ChildrenMargin = 24
type Controller interface {
MessageRowSelected(*Row, cchat.ServerMessage)
}
// Children is a children server with a reference to the parent.
type Children struct {
*gtk.Revealer
Main *gtk.Box
List cchat.ServerList
rowctrl Controller
Rows []*Row
ParentRow *Row
}
func NewChildren(parent *Row, list cchat.ServerList, ctrl Controller) *Children {
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
main.SetMarginStart(ChildrenMargin)
main.Show()
rev, _ := gtk.RevealerNew()
rev.SetRevealChild(false)
rev.Add(main)
rev.Show()
children := &Children{
Revealer: rev,
Main: main,
List: list,
rowctrl: ctrl,
ParentRow: parent,
}
if err := list.Servers(children); err != nil {
log.Error(errors.Wrap(err, "Failed to get servers"))
}
return children
}
func (c *Children) SetServers(servers []cchat.Server) {
gts.ExecAsync(func() {
// Save the current state.
var oldID string
for _, row := range c.Rows {
if row.GetActive() {
oldID = row.Server.ID()
break
}
}
// Update the server list.
for _, row := range c.Rows {
c.Main.Remove(row)
}
c.Rows = make([]*Row, len(servers))
for i, server := range servers {
row := NewRow(c, server, c.rowctrl)
c.Rows[i] = row
c.Main.Add(row)
}
// Update parent reference? Only if it's activated.
if oldID != "" {
for _, row := range c.Rows {
if row.Server.ID() == oldID {
row.Button.SetActive(true)
}
}
}
})
}

View file

@ -1,100 +0,0 @@
package server
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
type Row struct {
*gtk.Box
Button *rich.ToggleButtonImage
Server cchat.Server
Parent *Children
ctrl Controller
// enum 1
message cchat.ServerMessage
// enum 2
children *Children
}
func NewRow(parent *Children, server cchat.Server, ctrl Controller) *Row {
button := rich.NewToggleButtonImage(text.Rich{}, "")
button.Box.SetHAlign(gtk.ALIGN_START)
button.SetRelief(gtk.RELIEF_NONE)
button.Show()
if err := server.Name(button); err != nil {
log.Error(errors.Wrap(err, "Failed to get the server name"))
button.SetLabel(text.Rich{Content: "Unknown"})
}
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.PackStart(button, false, false, 0)
box.Show()
primitives.AddClass(box, "server")
// TODO: images
var row = &Row{
Box: box,
Button: button,
Server: server,
Parent: parent,
ctrl: ctrl,
}
switch server := server.(type) {
case cchat.ServerList:
row.children = NewChildren(row, server, ctrl)
box.PackStart(row.children, false, false, 0)
primitives.AddClass(box, "server-list")
case cchat.ServerMessage:
row.message = server
primitives.AddClass(box, "server-message")
}
button.Connect("clicked", row.onClick)
return row
}
func (row *Row) GetActive() bool {
return row.Button.GetActive()
}
func (row *Row) onClick() {
switch {
// If the server is a message server. We're only selected if the button is
// pressed.
case row.message != nil && row.GetActive():
row.ctrl.MessageRowSelected(row, row.message)
// If the server is a list of smaller servers.
case row.children != nil:
row.children.SetRevealChild(!row.children.GetRevealChild())
}
}
func (r *Row) Breadcrumb() string {
var label = r.Button.GetLabel().Content
// Does the row have a parent?
if r.Parent != nil {
return r.Parent.ParentRow.Breadcrumb() + "/" + label
}
return label
}

View file

@ -5,6 +5,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
@ -25,15 +26,17 @@ type Row struct {
Servers *server.Children
ctrl Controller
ctrl Controller
parent breadcrumb.Breadcrumber
}
func New(ses cchat.Session, ctrl Controller) *Row {
func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Row {
row := &Row{
Session: ses,
ctrl: ctrl,
parent: parent,
}
row.Servers = server.NewChildren(ses, row)
row.Servers = server.NewChildren(row, ses, row)
row.Button = rich.NewToggleButtonImage(text.Rich{}, "")
row.Button.Box.SetHAlign(gtk.ALIGN_START)
@ -67,3 +70,7 @@ func New(ses cchat.Session, ctrl Controller) *Row {
func (r *Row) MessageRowSelected(server *server.Row, smsg cchat.ServerMessage) {
r.ctrl.MessageRowSelected(r, server, smsg)
}
func (r *Row) Breadcrumb() breadcrumb.Breadcrumb {
return breadcrumb.Try(r.parent, r.Button.GetLabel().Content)
}

1
internal/ui/style.css Normal file
View file

@ -0,0 +1 @@
headerbar { padding: 0; }

View file

@ -10,8 +10,14 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/gotk3/gotk3/gtk"
"github.com/markbates/pkger"
)
func init() {
// Load the local CSS.
gts.LoadCSS(pkger.Include("/internal/ui/style.css"))
}
const LeftWidth = 220
type App struct {
@ -54,7 +60,7 @@ func (app *App) MessageRowSelected(ses *session.Row, srv *server.Row, smsg cchat
app.lastRowHighlighter = srv.Button.SetActive
app.lastRowHighlighter(true)
log.Println("Breadcrumb:")
app.header.SetBreadcrumb(srv.Breadcrumb())
// Show the messages.
app.window.MessageView.JoinServer(ses.Session, smsg)

12
pkged.go Normal file
View file

@ -0,0 +1,12 @@
// Code generated by pkger; DO NOT EDIT.
// +build !skippkger
package main
import (
"github.com/markbates/pkger"
"github.com/markbates/pkger/pkging/mem"
)
var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec58dd8fdb380eff570a3da7a366da29ae792eaeb88739146d713860512c688996b5d1d7ea639a7430fffb42b293c8f99ccc2eb058c00fb145f2474aa4258ae12391a6b5812c1e8990b14bcd0db39a7209da1ade246f9053c63a88af455c66d447e9c982d0ce6adcc0e857e6a58ba10756f019f98f76d6c7cf103bb278ce0433f25fd0481644833464463e5a461684ccc837f002e3c1cc37c2d2469a91852fd61e024f2ff11e22ebc8e2177243becfc8d7080ac922fa8403f105215843162464ea15478786a361ebc5abca21619bd4b6a02cedd02399914ff6df5261c886b32f37c266f37d3c0af77234a83411bd0145450c64769586b2e24a8d242f2b68cb96175134a07f900c4376f823bae26d935a69c98c34eb88d91766b5f318026d1544ac19e2a774853611a4414f950c7160e0aa8cfcda45bb1d50e82df60493ae43bfa3792de4017604b231c96fefeee61f0e18bb0821ff019e877d9852d245c9769c4e43456dd53d189ea2544744213551e14ea0f9dd8ec87a15c5de5544ed40e8603ea26eefde8fe8bbf96d45ef4d195515a7d5dd9b0f638abaa55c911941c32c974654430ac1cc6bba8180efdf8d38d2805fd71c619b9aecb0364e7fcb27aea21dea4c7a6f7d5e65abe378177e06a5a4e1e8a9b0af73bcace610610cfa7f02f3c36684b20c8aefe737f29507e8fa43da250d46febcbc92b1da12d7beff047f7f46d853c898974c44358600e2da48ec1429a46803f356a997dbc83908d8b5dfbd32208d4b2f50775e6a19e5035ebb8192a45eb2ee7aad2145bf583187fbe5d3d2c62370e6936e5e6e236008b2a4893f67a0d0e5c6f82b2ebf82a2018546f38c7cb0bb2b2f2287cbef5ccd3192f126059a1ffb02cb831486e60960bd2f8ccbb7c39381f4f6b458f0e519a192cd19693ca3eac088bd6935f865031103754bb1ffa5f6844742b18fd8ee050d2e9c87baa53848b4c73134446ef7acb9a5a0db3babe2ff0495afa87c131d4de587721a90798cbf56c7d62a30e2c67a4157656b54891e8c48432ebd8caa93de697404711450cdd541e88617659ebdcdb54617754ec75b33b9fa03256a1673a9265b1d83f5b166198cd143f17acbb3a104b666b93ef557e5d658c563ab904525e3881da4110a5b2545379a35ac0303a528ae90a17938264aa69444bb206188fde5574a5d69a9b443c1d7b33594a4995fb49162372c99a28c87624f4b8dc38beaa4a27450825218bf271b913b2f4d84a6d4310663ffa45d8cae1a96c7267a5be666c5032f7f49e76d29a9339d7c9694dd6c4309403f4aa11c3ed77b915fb4950a077a886f19095cb9ed8086b5899023e59389bd63c388b272d637d4369210ad2e15f5816408e1013facf37287ad13a267b67cb310f3f129a2b561c36b677ef8926446ca6e0e0c8c293e0ecb4c4632cbab114db19dbf1fd3ff2a648036e31ed0709bcbd0eaa46cfe9974c03ab87df33c94b36a3d7ffbe6ee02babcf2b17a2e6e53ad9f0327ff809b7f396770dd92b7e711877f70ce802f789c77263721ff76c5e229e076ef8b54ced6459cf376b5be00bca59d8352039c42496ee08438acc390eb8e49fbed872c79a48de4d2a793d12ad0e8c184d67a7d0eb4d9a3d9e0737026dbfb3e23df30c46dfbc224a57ad6b677d1b3ee2dcf8b5c3c926bda3bf720cda6bd725d2be993bdb7fcb9782aec8db6bca8fd0f7d291417647e337f479e9e9e66a4ed9d7b7c9a9152304c8dada9b13535b6a6c6d6d4d89a1a5b53636b6a6c4d8dada9b13535b6a6c6d6d4d89a1a5b53636b6a6c4d8dada9b1f50f686cfd010000ffff010000ffff0d1f6f41ce250000`)))