Fixed minor bugs; improved appearances; removed some dead code
This commit is contained in:
parent
ab4c949428
commit
f2f59b3c2b
2
go.mod
2
go.mod
|
@ -8,7 +8,7 @@ require (
|
||||||
github.com/Xuanwo/go-locale v0.2.0
|
github.com/Xuanwo/go-locale v0.2.0
|
||||||
github.com/alecthomas/chroma v0.7.3
|
github.com/alecthomas/chroma v0.7.3
|
||||||
github.com/diamondburned/cchat v0.0.42
|
github.com/diamondburned/cchat v0.0.42
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200708091545-601e8abeb23b
|
github.com/diamondburned/cchat-discord v0.0.0-20200709041349-1e137df6de2c
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3
|
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200708012333-53c9e45dd28b
|
github.com/diamondburned/imgutil v0.0.0-20200708012333-53c9e45dd28b
|
||||||
github.com/goodsign/monday v1.0.0
|
github.com/goodsign/monday v1.0.0
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -60,6 +60,10 @@ github.com/diamondburned/cchat-discord v0.0.0-20200708083530-d0e43cc63b03 h1:Xx4
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200708083530-d0e43cc63b03/go.mod h1:hjaCeQfhSqANH+ZtxXPMWzpgobb4syy2VcqDK4PXcPU=
|
github.com/diamondburned/cchat-discord v0.0.0-20200708083530-d0e43cc63b03/go.mod h1:hjaCeQfhSqANH+ZtxXPMWzpgobb4syy2VcqDK4PXcPU=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200708091545-601e8abeb23b h1:c01OHXPE/HXcZdaPDgNNzmz3xN7MC0yIyL5i+6qILAc=
|
github.com/diamondburned/cchat-discord v0.0.0-20200708091545-601e8abeb23b h1:c01OHXPE/HXcZdaPDgNNzmz3xN7MC0yIyL5i+6qILAc=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200708091545-601e8abeb23b/go.mod h1:IDBm0RqdQUASjPGP/RQkhmC1nGhGTTFoaHn9NbD/l7I=
|
github.com/diamondburned/cchat-discord v0.0.0-20200708091545-601e8abeb23b/go.mod h1:IDBm0RqdQUASjPGP/RQkhmC1nGhGTTFoaHn9NbD/l7I=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20200708212314-e08c31b895a6 h1:Nc9B6i7siInzh/uu3bTu7mcw7ckcFDTu4TtSNoxUMis=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20200708212314-e08c31b895a6/go.mod h1:QHPtnxNrnMFCYB/b9kUP93D30Kf3AuGmkM91tScIpB8=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20200709041349-1e137df6de2c h1:4F7IJqMfpF02/j6ZbVO9x29rWExQncLHFbxZrsAhhvM=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20200709041349-1e137df6de2c/go.mod h1:QHPtnxNrnMFCYB/b9kUP93D30Kf3AuGmkM91tScIpB8=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3 h1:xr07/2cwINyrMqh92pQQJVDfQqG0u6gHAK+ZcGfpSew=
|
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3 h1:xr07/2cwINyrMqh92pQQJVDfQqG0u6gHAK+ZcGfpSew=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3/go.mod h1:SRu3OOeggELFr2Wd3/+SpYV1eNcvSk2LBhM70NOZSG8=
|
github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3/go.mod h1:SRu3OOeggELFr2Wd3/+SpYV1eNcvSk2LBhM70NOZSG8=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI=
|
github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI=
|
||||||
|
@ -72,6 +76,8 @@ github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xC
|
||||||
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
|
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
|
||||||
github.com/diamondburned/ningen v0.1.1-0.20200708090333-227e90d19851 h1:xf1aLPnwK/Yn2z7dBIgQROSVOEc2wtivgnnwBItdEVM=
|
github.com/diamondburned/ningen v0.1.1-0.20200708090333-227e90d19851 h1:xf1aLPnwK/Yn2z7dBIgQROSVOEc2wtivgnnwBItdEVM=
|
||||||
github.com/diamondburned/ningen v0.1.1-0.20200708090333-227e90d19851/go.mod h1:FNezDLQIhoDS+RkXLSQ7dJNrt6BW/nVl1krzDgWMQwg=
|
github.com/diamondburned/ningen v0.1.1-0.20200708090333-227e90d19851/go.mod h1:FNezDLQIhoDS+RkXLSQ7dJNrt6BW/nVl1krzDgWMQwg=
|
||||||
|
github.com/diamondburned/ningen v0.1.1-0.20200708211706-57c712372ede h1:qRmfQCOS+ZnH4G0+8O09PUx3HQTdQwzsDoo1ucTgm2E=
|
||||||
|
github.com/diamondburned/ningen v0.1.1-0.20200708211706-57c712372ede/go.mod h1:FNezDLQIhoDS+RkXLSQ7dJNrt6BW/nVl1krzDgWMQwg=
|
||||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
|
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
# c
|
|
||||||
|
|
||||||
**HERE BE DRAGONS!!**
|
|
||||||
|
|
||||||
This package and all its subpackages contain either C code or Cgo code. This
|
|
||||||
bugs me and I hate it. PROCEED WITH CAUTION!
|
|
|
@ -1,101 +0,0 @@
|
||||||
package labelutils
|
|
||||||
|
|
||||||
// #cgo pkg-config: gdk-3.0 gio-2.0 glib-2.0 gobject-2.0 gtk+-3.0
|
|
||||||
// #include <glib.h>
|
|
||||||
// #include <gtk/gtk.h>
|
|
||||||
// #include <pango/pango.h>
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/gotk3/gotk3/gtk"
|
|
||||||
"github.com/gotk3/gotk3/pango"
|
|
||||||
)
|
|
||||||
|
|
||||||
func gbool(b bool) C.gboolean {
|
|
||||||
if b {
|
|
||||||
return C.gboolean(C.TRUE)
|
|
||||||
} else {
|
|
||||||
return C.gboolean(C.FALSE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Attribute = func() *C.PangoAttribute
|
|
||||||
|
|
||||||
func InsertHyphens(hyphens bool) Attribute {
|
|
||||||
return func() *C.PangoAttribute {
|
|
||||||
return C.pango_attr_insert_hyphens_new(gbool(hyphens))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Scale(factor float64) Attribute {
|
|
||||||
return func() *C.PangoAttribute {
|
|
||||||
return C.pango_attr_scale_new(C.double(factor))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Underline(underline pango.Underline) Attribute {
|
|
||||||
return func() *C.PangoAttribute {
|
|
||||||
return C.pango_attr_underline_new(C.PangoUnderline(underline))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Strikethrough(strikethrough bool) Attribute {
|
|
||||||
return func() *C.PangoAttribute {
|
|
||||||
return C.pango_attr_strikethrough_new(gbool(strikethrough))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const u16divu8 = 65535 / 255
|
|
||||||
|
|
||||||
func rgb(hex uint32) (r, g, b uint16) {
|
|
||||||
r = uint16(hex>>16&255) * u16divu8
|
|
||||||
g = uint16(hex>>8&255) * u16divu8
|
|
||||||
b = uint16(hex&255) * u16divu8
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func Background(hex uint32) Attribute {
|
|
||||||
r, g, b := rgb(hex)
|
|
||||||
|
|
||||||
return func() *C.PangoAttribute {
|
|
||||||
return C.pango_attr_background_new(C.guint16(r), C.guint16(g), C.guint16(b))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Foreground(hex uint32) Attribute {
|
|
||||||
r, g, b := rgb(hex)
|
|
||||||
|
|
||||||
return func() *C.PangoAttribute {
|
|
||||||
return C.pango_attr_foreground_new(C.guint16(r), C.guint16(g), C.guint16(b))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Style(style pango.Style) Attribute {
|
|
||||||
return func() *C.PangoAttribute {
|
|
||||||
return C.pango_attr_style_new(C.PangoStyle(style))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Family(family string) Attribute {
|
|
||||||
return func() *C.PangoAttribute {
|
|
||||||
str := C.CString(family)
|
|
||||||
defer C.free(unsafe.Pointer(str))
|
|
||||||
return C.pango_attr_family_new(str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddAttr(l *gtk.Label, attrs ...Attribute) {
|
|
||||||
attrlist := C.pango_attr_list_new()
|
|
||||||
defer C.pango_attr_list_unref(attrlist)
|
|
||||||
|
|
||||||
for _, attr := range attrs {
|
|
||||||
// attr() should not unref; insert is transfer-full
|
|
||||||
// https://discourse.gnome.org/t/pango-how-to-turn-off-hyphenation-for-char-wrapping/2101/2
|
|
||||||
C.pango_attr_list_insert(attrlist, attr())
|
|
||||||
}
|
|
||||||
|
|
||||||
v := (*C.GtkLabel)(unsafe.Pointer(l.Native()))
|
|
||||||
C.gtk_label_set_attributes(v, attrlist)
|
|
||||||
}
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/c/labelutils"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/dialog"
|
"github.com/diamondburned/cchat-gtk/internal/ui/dialog"
|
||||||
|
@ -87,7 +86,7 @@ func BindTooltip(connector WidgetConnector) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlPrompt = `This link leads to the following URL:
|
const urlPrompt = `This link leads to the following URL:
|
||||||
<span weight="bold"><a href="%[1]s">%[1]s</a></span>
|
<span weight="bold" insert_hyphens="false"><a href="%[1]s">%[1]s</a></span>
|
||||||
Click <b>Open</b> to proceed.`
|
Click <b>Open</b> to proceed.`
|
||||||
|
|
||||||
var warnLabelCSS = primitives.PrepareCSS(`
|
var warnLabelCSS = primitives.PrepareCSS(`
|
||||||
|
@ -109,9 +108,6 @@ func PromptOpen(uri string) {
|
||||||
// Style the label.
|
// Style the label.
|
||||||
primitives.AttachCSS(l, warnLabelCSS)
|
primitives.AttachCSS(l, warnLabelCSS)
|
||||||
|
|
||||||
// Disable hyphens on line wraps.
|
|
||||||
labelutils.AddAttr(l, labelutils.InsertHyphens(false))
|
|
||||||
|
|
||||||
open := func() {
|
open := func() {
|
||||||
if err := open.Start(uri); err != nil {
|
if err := open.Start(uri); err != nil {
|
||||||
log.Error(errors.Wrap(err, "Failed to open URL after confirm"))
|
log.Error(errors.Wrap(err, "Failed to open URL after confirm"))
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
package completion
|
package completion
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/diamondburned/imgutil"
|
"github.com/diamondburned/imgutil"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ImageSize = 25
|
ImageSmall = 25
|
||||||
|
ImageLarge = 40
|
||||||
ImagePadding = 6
|
ImagePadding = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,30 +56,53 @@ func (v *View) Update(words []string, i int) []gtk.IWidget {
|
||||||
var widgets = make([]gtk.IWidget, len(v.entries))
|
var widgets = make([]gtk.IWidget, len(v.entries))
|
||||||
|
|
||||||
for i, entry := range v.entries {
|
for i, entry := range v.entries {
|
||||||
|
// Container that holds the label.
|
||||||
|
lbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||||
|
lbox.SetVAlign(gtk.ALIGN_CENTER)
|
||||||
|
lbox.Show()
|
||||||
|
|
||||||
|
// Label for the primary text.
|
||||||
l := rich.NewLabel(entry.Text)
|
l := rich.NewLabel(entry.Text)
|
||||||
l.Show()
|
l.Show()
|
||||||
|
lbox.PackStart(l, false, false, 0)
|
||||||
|
|
||||||
img, _ := gtk.ImageNew()
|
// Get the iamge size so we can change and use if needed. The default
|
||||||
|
var size = ImageSmall
|
||||||
|
if !entry.Secondary.Empty() {
|
||||||
|
size = ImageLarge
|
||||||
|
|
||||||
|
s := rich.NewLabel(text.Rich{})
|
||||||
|
s.SetMarkup(fmt.Sprintf(
|
||||||
|
`<span alpha="50%%" size="small">%s</span>`,
|
||||||
|
parser.RenderMarkup(entry.Secondary),
|
||||||
|
))
|
||||||
|
s.Show()
|
||||||
|
|
||||||
|
lbox.PackStart(s, false, false, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||||
|
b.PackEnd(lbox, true, true, ImagePadding)
|
||||||
|
b.Show()
|
||||||
|
|
||||||
// Do we have an icon?
|
// Do we have an icon?
|
||||||
if entry.IconURL != "" {
|
if entry.IconURL != "" {
|
||||||
|
img, _ := gtk.ImageNew()
|
||||||
img.SetMarginStart(ImagePadding)
|
img.SetMarginStart(ImagePadding)
|
||||||
img.SetSizeRequest(ImageSize, ImageSize)
|
img.SetSizeRequest(size, size)
|
||||||
img.Show()
|
img.Show()
|
||||||
|
|
||||||
|
// Prepend the image into the box.
|
||||||
|
b.PackEnd(img, false, false, 0)
|
||||||
|
|
||||||
var pps []imgutil.Processor
|
var pps []imgutil.Processor
|
||||||
if !entry.Image {
|
if !entry.Image {
|
||||||
pps = ppIcon
|
pps = ppIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
httputil.AsyncImageSized(img, entry.IconURL, ImageSize, ImageSize, pps...)
|
httputil.AsyncImageSized(img, entry.IconURL, size, size, pps...)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
|
||||||
b.PackStart(img, false, false, 0) // image has pad left
|
|
||||||
b.PackStart(l, true, true, ImagePadding)
|
|
||||||
b.Show()
|
|
||||||
|
|
||||||
widgets[i] = b
|
widgets[i] = b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/c/labelutils"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/humanize"
|
"github.com/diamondburned/cchat-gtk/internal/humanize"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/imgview"
|
"github.com/diamondburned/cchat-gtk/internal/ui/imgview"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
@ -91,7 +90,6 @@ 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.Show()
|
ts.Show()
|
||||||
|
|
||||||
user, _ := gtk.LabelNew("")
|
user, _ := gtk.LabelNew("")
|
||||||
|
@ -100,7 +98,6 @@ 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.Show()
|
user.Show()
|
||||||
|
|
||||||
content, _ := gtk.LabelNew("")
|
content, _ := gtk.LabelNew("")
|
||||||
|
@ -110,8 +107,14 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
content.SetSelectable(true)
|
content.SetSelectable(true)
|
||||||
content.Show()
|
content.Show()
|
||||||
|
|
||||||
// Never insert hyphens on line breaks in the message content.
|
// content.Connect("grab-notify", func(l *gtk.Label, grabbed bool) {
|
||||||
labelutils.AddAttr(content, labelutils.InsertHyphens(false))
|
// if grabbed {
|
||||||
|
// // Hack to stop the label from selecting everything after being
|
||||||
|
// // refocused.
|
||||||
|
// content.SetSelectable(false)
|
||||||
|
// gts.ExecAsync(func() { content.SetSelectable(true) })
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
// Add CSS classes.
|
// Add CSS classes.
|
||||||
primitives.AddClass(ts, "message-time")
|
primitives.AddClass(ts, "message-time")
|
||||||
|
|
|
@ -193,7 +193,10 @@ func (v *View) AddPresendMessage(msg input.PresendMessage) func(error) {
|
||||||
|
|
||||||
// AuthorEvent should be called on message create/update/delete.
|
// AuthorEvent should be called on message create/update/delete.
|
||||||
func (v *View) AuthorEvent(author cchat.MessageAuthor) {
|
func (v *View) AuthorEvent(author cchat.MessageAuthor) {
|
||||||
v.Typing.RemoveAuthor(author)
|
// Remove the author from the typing list if it's not nil.
|
||||||
|
if author != nil {
|
||||||
|
v.Typing.RemoveAuthor(author)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LatestMessageFrom returns the last message ID with that author.
|
// LatestMessageFrom returns the last message ID with that author.
|
||||||
|
|
|
@ -3,56 +3,82 @@ package attrmap
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppendMap struct {
|
type AppendMap struct {
|
||||||
appended map[int]string
|
appended map[int]string // for opening tags
|
||||||
indices []int
|
prepended map[int]string // for closing tags
|
||||||
|
indices []int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAppendedMap() AppendMap {
|
func NewAppendedMap() AppendMap {
|
||||||
return AppendMap{
|
return AppendMap{
|
||||||
appended: make(map[int]string),
|
appended: map[int]string{},
|
||||||
indices: []int{},
|
prepended: map[int]string{},
|
||||||
|
indices: []int{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppendMap) Span(start, end int, attr string) {
|
func (a *AppendMap) appendIndex(ind int) {
|
||||||
a.Add(start, `<span `+attr+`>`)
|
// Backwards search which should make things faster.
|
||||||
a.Add(end, "</span>")
|
for i := len(a.indices) - 1; i >= 0; i-- {
|
||||||
|
if a.indices[i] == ind {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.indices = append(a.indices, ind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppendMap) Span(start, end int, attrs ...string) {
|
||||||
|
a.Openf(start, "<span %s>", strings.Join(attrs, " "))
|
||||||
|
a.Close(end, "</span>")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppendMap) Pair(start, end int, open, close string) {
|
func (a *AppendMap) Pair(start, end int, open, close string) {
|
||||||
a.Add(start, open)
|
a.Open(start, open)
|
||||||
a.Add(end, close)
|
a.Close(end, close)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppendMap) Addf(ind int, f string, argv ...interface{}) {
|
func (a *AppendMap) Openf(ind int, f string, argv ...interface{}) {
|
||||||
a.Add(ind, fmt.Sprintf(f, argv...))
|
a.Open(ind, fmt.Sprintf(f, argv...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppendMap) Pad(ind int) {
|
func (a *AppendMap) Open(ind int, attr string) {
|
||||||
a.Add(ind, "\n")
|
if str, ok := a.appended[ind]; ok {
|
||||||
}
|
a.appended[ind] = str + attr // append
|
||||||
|
|
||||||
func (a *AppendMap) Add(ind int, attr string) {
|
|
||||||
if _, ok := a.appended[ind]; ok {
|
|
||||||
a.appended[ind] += attr
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a.appended[ind] = attr
|
a.appended[ind] = attr
|
||||||
a.indices = append(a.indices, ind)
|
a.appendIndex(ind)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AppendMap) Get(ind int) string {
|
func (a *AppendMap) Close(ind int, attr string) {
|
||||||
return a.appended[ind]
|
if str, ok := a.prepended[ind]; ok {
|
||||||
|
a.prepended[ind] = attr + str // prepend
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.prepended[ind] = attr
|
||||||
|
a.appendIndex(ind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AppendMap) Get(ind int) (tags string) {
|
||||||
|
if t, ok := a.appended[ind]; ok {
|
||||||
|
tags += t
|
||||||
|
}
|
||||||
|
if t, ok := a.prepended[ind]; ok {
|
||||||
|
tags += t
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppendMap) Finalize(strlen int) []int {
|
func (a *AppendMap) Finalize(strlen int) []int {
|
||||||
// make sure there's always a closing tag at the end so the entire string
|
// make sure there's always a closing tag at the end so the entire string
|
||||||
// gets flushed.
|
// gets flushed.
|
||||||
a.Add(strlen, "")
|
a.Close(strlen, "")
|
||||||
sort.Ints(a.indices)
|
sort.Ints(a.indices)
|
||||||
return a.indices
|
return a.indices
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,11 @@ func Tokenize(language, source string) chroma.Iterator {
|
||||||
|
|
||||||
func Segments(appendmap *attrmap.AppendMap, src string, seg text.Codeblocker) {
|
func Segments(appendmap *attrmap.AppendMap, src string, seg text.Codeblocker) {
|
||||||
var start, end = seg.Bounds()
|
var start, end = seg.Bounds()
|
||||||
appendmap.Span(start, end, `font_family="monospace"`)
|
appendmap.Span(
|
||||||
|
start, end,
|
||||||
|
`font_family="monospace"`,
|
||||||
|
`insert_hyphens="false"`, // all my homies hate hyphens
|
||||||
|
)
|
||||||
|
|
||||||
if i := Tokenize(seg.CodeblockLanguage(), src[start:end]); i != nil {
|
if i := Tokenize(seg.CodeblockLanguage(), src[start:end]); i != nil {
|
||||||
fmtter.segments(appendmap, start, i)
|
fmtter.segments(appendmap, start, i)
|
||||||
|
@ -92,13 +96,13 @@ func (f *formatter) segments(appendmap *attrmap.AppendMap, offset int, iter chro
|
||||||
attr := f.styleAttr(token.Type)
|
attr := f.styleAttr(token.Type)
|
||||||
|
|
||||||
if attr != "" {
|
if attr != "" {
|
||||||
appendmap.Addf(offset, `<span %s>`, attr)
|
appendmap.Openf(offset, `<span %s>`, attr)
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += len(token.Value)
|
offset += len(token.Value)
|
||||||
|
|
||||||
if attr != "" {
|
if attr != "" {
|
||||||
appendmap.Add(offset, "</span>")
|
appendmap.Close(offset, "</span>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,17 +65,26 @@ func RenderMarkup(content text.Rich) string {
|
||||||
start, end := segment.Bounds()
|
start, end := segment.Bounds()
|
||||||
|
|
||||||
if segment, ok := segment.(text.Linker); ok {
|
if segment, ok := segment.(text.Linker); ok {
|
||||||
appended.Addf(start, `<a href="%s">`, html.EscapeString(segment.Link()))
|
appended.Openf(start, `<a href="%s">`, html.EscapeString(segment.Link()))
|
||||||
appended.Add(end, "</a>")
|
appended.Close(end, "</a>")
|
||||||
}
|
}
|
||||||
|
|
||||||
if segment, ok := segment.(text.Imager); ok {
|
if segment, ok := segment.(text.Imager); ok {
|
||||||
// Ends don't matter with images.
|
// Ends don't matter with images.
|
||||||
appended.Add(start, composeImageMarkup(segment))
|
appended.Open(start, composeImageMarkup(segment))
|
||||||
}
|
}
|
||||||
|
|
||||||
if segment, ok := segment.(text.Colorer); ok {
|
if segment, ok := segment.(text.Colorer); ok {
|
||||||
appended.Span(start, end, fmt.Sprintf(`color="#%06X"`, segment.Color()))
|
var attrs = []string{fmt.Sprintf(`color="#%06X"`, segment.Color())}
|
||||||
|
// If the color segment only covers a segment, then add some more
|
||||||
|
// formatting.
|
||||||
|
if start > 0 && end < len(content.Content) {
|
||||||
|
attrs = append(attrs,
|
||||||
|
`bgalpha="10%"`,
|
||||||
|
fmt.Sprintf(`bgcolor="#%06X"`, segment.Color()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
appended.Span(start, end, attrs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if segment, ok := segment.(text.Attributor); ok {
|
if segment, ok := segment.(text.Attributor); ok {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test no longer works, and should not work anyway.
|
||||||
|
|
||||||
|
// 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" bgalpha="10%" bgcolor="#FFFFFF">nd<span color="#F7A8B8" bgalpha="10%" bgcolor="#F7A8B8"></span>om</span></span>
|
||||||
|
// `<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
|
||||||
|
}
|
|
@ -1,170 +0,0 @@
|
||||||
package parser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
"github.com/gotk3/gotk3/gdk"
|
|
||||||
"github.com/gotk3/gotk3/gtk"
|
|
||||||
"github.com/gotk3/gotk3/pango"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/skratchdot/open-golang/open"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AppendEditBadge(b *gtk.TextBuffer, editedAt time.Time) {
|
|
||||||
r := newRenderCtx(b)
|
|
||||||
|
|
||||||
t := r.createTag(map[string]interface{}{
|
|
||||||
"scale": 0.84,
|
|
||||||
"scale-set": true,
|
|
||||||
"foreground": "#808080", // blue-ish URL color
|
|
||||||
})
|
|
||||||
|
|
||||||
bindClicker(t, func(_ *gtk.TextView, ev *gdk.Event) {
|
|
||||||
switch ev := gdk.EventMotionNewFromEvent(ev); ev.Type() {
|
|
||||||
case gdk.EVENT_PROXIMITY_IN:
|
|
||||||
log.Println("Proximity in")
|
|
||||||
case gdk.EVENT_PROXIMITY_OUT:
|
|
||||||
log.Println("Proximity out")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.InsertWithTag(b.GetEndIter(), " (edited)", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderTextBuffer(b *gtk.TextBuffer, content text.Rich) {
|
|
||||||
r := newRenderCtx(b)
|
|
||||||
b.SetText(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
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, segment := range content.Segments {
|
|
||||||
start, end := segment.Bounds()
|
|
||||||
|
|
||||||
switch segment := segment.(type) {
|
|
||||||
case text.Attributor:
|
|
||||||
r.tagAttr(start, end, segment.Attribute())
|
|
||||||
|
|
||||||
case text.Colorer:
|
|
||||||
color := fmt.Sprintf("#%06X", segment.Color())
|
|
||||||
r.applyProps(start, end, map[string]interface{}{
|
|
||||||
"foreground": color,
|
|
||||||
})
|
|
||||||
|
|
||||||
case text.Codeblocker:
|
|
||||||
r.applyProps(start, end, map[string]interface{}{
|
|
||||||
"family": "Monospace",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type renderCtx struct {
|
|
||||||
b *gtk.TextBuffer
|
|
||||||
t *gtk.TextTagTable
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRenderCtx(b *gtk.TextBuffer) *renderCtx {
|
|
||||||
t, _ := b.GetTagTable()
|
|
||||||
return &renderCtx{b, t}
|
|
||||||
}
|
|
||||||
|
|
||||||
type OnClicker func(tv *gtk.TextView, ev *gdk.Event)
|
|
||||||
|
|
||||||
func bindClicker(v primitives.Connector, fn OnClicker) {
|
|
||||||
v.Connect("event", func(_ *gtk.TextTag, tv *gtk.TextView, ev *gdk.Event) {
|
|
||||||
evButton := gdk.EventButtonNewFromEvent(ev)
|
|
||||||
if evButton.Type() != gdk.EVENT_BUTTON_RELEASE || evButton.Button() != gdk.BUTTON_PRIMARY {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fn(tv, ev)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *renderCtx) applyHyperlink(start, end int, url string) {
|
|
||||||
t := r.createTag(map[string]interface{}{
|
|
||||||
"underline": pango.UNDERLINE_SINGLE,
|
|
||||||
"foreground": "#3F7CE0", // blue-ish URL color
|
|
||||||
})
|
|
||||||
|
|
||||||
bindClicker(t, func(*gtk.TextView, *gdk.Event) {
|
|
||||||
if err := open.Start(url); err != nil {
|
|
||||||
log.Error(errors.Wrap(err, "Failed to open image URL"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *renderCtx) applyProps(start, end int, props map[string]interface{}) {
|
|
||||||
tag := r.createTag(props)
|
|
||||||
r.applyTag(start, end, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *renderCtx) applyTag(start, end int, tag *gtk.TextTag) {
|
|
||||||
istart, iend := r.iters(start, end)
|
|
||||||
r.b.ApplyTag(tag, istart, iend)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *renderCtx) createTag(props map[string]interface{}) *gtk.TextTag {
|
|
||||||
t, _ := gtk.TextTagNew("")
|
|
||||||
r.t.Add(t)
|
|
||||||
|
|
||||||
if props != nil {
|
|
||||||
for k, v := range props {
|
|
||||||
t.SetProperty(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *renderCtx) iters(start, end int) (is, ie *gtk.TextIter) {
|
|
||||||
return r.b.GetIterAtOffset(start), r.b.GetIterAtOffset(end)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *renderCtx) tagAttr(start, end int, attr text.Attribute) {
|
|
||||||
var props = tagAttrMap(attr)
|
|
||||||
if props == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.applyTag(start, end, r.createTag(props))
|
|
||||||
}
|
|
||||||
|
|
||||||
func tagAttrMap(attr text.Attribute) map[string]interface{} {
|
|
||||||
if attr == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var props = make(map[string]interface{}, 1)
|
|
||||||
|
|
||||||
if attr.Has(text.AttrBold) {
|
|
||||||
props["weight"] = pango.WEIGHT_BOLD
|
|
||||||
}
|
|
||||||
if attr.Has(text.AttrItalics) {
|
|
||||||
props["style"] = pango.STYLE_ITALIC
|
|
||||||
}
|
|
||||||
if attr.Has(text.AttrUnderline) {
|
|
||||||
props["underline"] = pango.UNDERLINE_SINGLE
|
|
||||||
}
|
|
||||||
if attr.Has(text.AttrStrikethrough) {
|
|
||||||
props["strikethrough"] = true
|
|
||||||
}
|
|
||||||
if attr.Has(text.AttrSpoiler) {
|
|
||||||
props["foreground"] = "#808080"
|
|
||||||
}
|
|
||||||
if attr.Has(text.AttrMonospace) {
|
|
||||||
props["family"] = "Monospace"
|
|
||||||
}
|
|
||||||
|
|
||||||
return props
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
Loading…
Reference in New Issue