mirror of
https://github.com/aljazceru/njump.git
synced 2025-12-18 14:54:24 +01:00
naïve text highlighting.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/fogleman/gg"
|
||||
"github.com/go-text/typesetting/di"
|
||||
@@ -31,6 +32,16 @@ const (
|
||||
scaleShift = 6
|
||||
)
|
||||
|
||||
// highlighting stuff
|
||||
type hlstate int
|
||||
|
||||
const (
|
||||
hlNormal hlstate = 0
|
||||
hlLink hlstate = 1
|
||||
hlMention hlstate = 2
|
||||
hlHashtag hlstate = 3
|
||||
)
|
||||
|
||||
var (
|
||||
supportedScripts = [nSupportedScripts]language.Script{
|
||||
language.Unknown,
|
||||
@@ -357,7 +368,7 @@ func shortenURLs(text string) string {
|
||||
// but also, the most important change was to make it "shape" the same text, twice, with the default font and with
|
||||
// the emoji font, then build an output of glyphs containing normal glyphs for when the referenced rune is not an
|
||||
// emoji and an emoji glyph for when it is.
|
||||
func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool) {
|
||||
func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool, []hlstate) {
|
||||
lang, script, dir, face := getLanguageAndScriptAndDirectionAndFont(rawText)
|
||||
|
||||
shaperLock.Lock()
|
||||
@@ -449,9 +460,15 @@ func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// this will be used to determine if we'll use a different color when rendering a glyph or not
|
||||
hlMask := make([]hlstate, len(emojiBuffer.Info))
|
||||
var hlState hlstate = hlNormal
|
||||
|
||||
// convert the shaped text into an output
|
||||
glyphs := make([]shaping.Glyph, len(mainBuffer.Info))
|
||||
for i := 0; i < len(glyphs); i++ {
|
||||
|
||||
// deciding if we'll render this as emoji or not
|
||||
var buf *harfbuzz.Buffer
|
||||
var font *harfbuzz.Font
|
||||
if emojiMask[i] {
|
||||
@@ -461,8 +478,57 @@ func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool) {
|
||||
buf = mainBuffer
|
||||
font = mainFont
|
||||
}
|
||||
|
||||
// current glyph specs
|
||||
glyph := buf.Info[i]
|
||||
|
||||
// naïve text highlighting
|
||||
switch hlState {
|
||||
case hlNormal:
|
||||
if glyph.Codepoint == '#' &&
|
||||
len(buf.Info) > i+1 &&
|
||||
unicode.In(buf.Info[i+1].Codepoint, unicode.Letter) {
|
||||
hlState = hlHashtag
|
||||
} else if glyph.Codepoint == 'h' &&
|
||||
len(buf.Info) > i+1 &&
|
||||
buf.Info[i+1].Codepoint == 't' &&
|
||||
buf.Info[i+2].Codepoint == 't' &&
|
||||
buf.Info[i+3].Codepoint == 'p' {
|
||||
|
||||
if buf.Info[i+4].Codepoint == 's' &&
|
||||
buf.Info[i+5].Codepoint == ':' &&
|
||||
buf.Info[i+6].Codepoint == '/' &&
|
||||
buf.Info[i+7].Codepoint == '/' &&
|
||||
buf.Info[i+8].Codepoint != ' ' {
|
||||
hlState = hlLink
|
||||
} else if buf.Info[i+4].Codepoint == ':' &&
|
||||
buf.Info[i+5].Codepoint == '/' &&
|
||||
buf.Info[i+6].Codepoint == '/' &&
|
||||
buf.Info[i+7].Codepoint != ' ' {
|
||||
hlState = hlLink
|
||||
}
|
||||
} else if glyph.Codepoint == '@' &&
|
||||
len(buf.Info) > i+1 &&
|
||||
unicode.In(buf.Info[i+1].Codepoint, unicode.Letter) {
|
||||
hlState = hlMention
|
||||
}
|
||||
case hlLink:
|
||||
if glyph.Codepoint == ' ' ||
|
||||
glyph.Codepoint == ',' {
|
||||
hlState = hlNormal
|
||||
}
|
||||
case hlMention:
|
||||
if !unicode.In(glyph.Codepoint, unicode.Letter) {
|
||||
hlState = hlNormal
|
||||
}
|
||||
case hlHashtag:
|
||||
if !unicode.In(glyph.Codepoint, unicode.Letter) {
|
||||
hlState = hlNormal
|
||||
}
|
||||
}
|
||||
hlMask[i] = hlState
|
||||
// ~
|
||||
|
||||
glyphs[i] = shaping.Glyph{
|
||||
ClusterIndex: glyph.Cluster,
|
||||
GlyphID: glyph.Glyph,
|
||||
@@ -500,7 +566,7 @@ func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool) {
|
||||
}
|
||||
out.RecalculateAll()
|
||||
|
||||
return out, emojiMask
|
||||
return out, emojiMask, hlMask
|
||||
}
|
||||
|
||||
// this function is copied from go-text/typesetting/shaping because shapeText needs it
|
||||
@@ -546,12 +612,13 @@ func countClusters(glyphs []shaping.Glyph, textLen int, dir di.Progression) {
|
||||
// this function is copied from go-text/render, but adapted to not require a "class" to be instantiated and also,
|
||||
// more importantly, to take an emojiMask parameter, with the same length as out.Glyphs, to determine when a
|
||||
// glyph should be rendered with the emoji font instead of with the default font
|
||||
func drawShapedRunAt(
|
||||
func drawShapedBlockAt(
|
||||
img draw.Image,
|
||||
fontSize int,
|
||||
clr color.Color,
|
||||
colors [4]color.Color,
|
||||
out shaping.Output,
|
||||
emojiMask []bool,
|
||||
hlMask []hlstate,
|
||||
maskBaseIndex int,
|
||||
startX,
|
||||
startY int,
|
||||
@@ -559,11 +626,17 @@ func drawShapedRunAt(
|
||||
scale := float32(fontSize) / float32(out.Face.Upem())
|
||||
|
||||
b := img.Bounds()
|
||||
scanner := rasterx.NewScannerGV(b.Dx(), b.Dy(), img, b)
|
||||
f := rasterx.NewFiller(b.Dx(), b.Dy(), scanner)
|
||||
f.SetColor(clr)
|
||||
|
||||
var fillers [4]*rasterx.Filler
|
||||
for i := range fillers {
|
||||
scanner := rasterx.NewScannerGV(b.Dx(), b.Dy(), img, b)
|
||||
fillers[i] = rasterx.NewFiller(b.Dx(), b.Dy(), scanner)
|
||||
fillers[i].SetColor(colors[i])
|
||||
}
|
||||
|
||||
x := float32(startX)
|
||||
y := float32(startY)
|
||||
|
||||
for i, g := range out.Glyphs {
|
||||
xPos := x + fixed266ToFloat(g.XOffset)
|
||||
yPos := y - fixed266ToFloat(g.YOffset)
|
||||
@@ -575,6 +648,8 @@ func drawShapedRunAt(
|
||||
currentScale = float32(fontSize) / float32(face.Upem())
|
||||
}
|
||||
|
||||
f := fillers[hlMask[maskBaseIndex+i]]
|
||||
|
||||
data := face.GlyphData(g.GlyphID)
|
||||
switch format := data.(type) {
|
||||
case api.GlyphOutline:
|
||||
@@ -588,7 +663,10 @@ func drawShapedRunAt(
|
||||
charsWritten++
|
||||
x += fixed266ToFloat(g.XAdvance)
|
||||
}
|
||||
f.Draw()
|
||||
|
||||
for _, filler := range fillers {
|
||||
filler.Draw()
|
||||
}
|
||||
|
||||
return charsWritten, int(math.Ceil(float64(x)))
|
||||
}
|
||||
|
||||
@@ -179,10 +179,9 @@ func drawImage(paragraphs []string, style Style, metadata sdk.ProfileMetadata, d
|
||||
|
||||
func drawText(paragraphs []string, width, height int, dynamicResize bool) image.Image {
|
||||
FONT_SIZE := 25
|
||||
color := color.RGBA{R: 255, G: 230, B: 238, A: 255}
|
||||
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
joinedContent := strings.Join(paragraphs, " \n") // The space before the \n is necessary
|
||||
joinedContent := strings.Join(paragraphs, " \n") // the space before the \n is necessary
|
||||
if dynamicResize && len(joinedContent) < 141 {
|
||||
fontSizeTest := 7.0
|
||||
step := 0.5
|
||||
@@ -209,7 +208,7 @@ func drawText(paragraphs []string, width, height int, dynamicResize bool) image.
|
||||
for _, paragraph := range paragraphs {
|
||||
rawText := []rune(paragraph)
|
||||
|
||||
shapedRunes, emojiMask := shapeText(rawText, FONT_SIZE)
|
||||
shapedRunes, emojiMask, hlMask := shapeText(rawText, FONT_SIZE)
|
||||
|
||||
var wrapper shaping.LineWrapper
|
||||
it := shaping.NewSliceIterator([]shaping.Output{shapedRunes})
|
||||
@@ -218,12 +217,18 @@ func drawText(paragraphs []string, width, height int, dynamicResize bool) image.
|
||||
totalCharsWritten := 0
|
||||
for _, line := range lines {
|
||||
for _, out := range line {
|
||||
charsWritten, _ := drawShapedRunAt(
|
||||
charsWritten, _ := drawShapedBlockAt(
|
||||
img,
|
||||
FONT_SIZE,
|
||||
color,
|
||||
[4]color.Color{
|
||||
color.RGBA{R: 255, G: 230, B: 238, A: 255}, // normal
|
||||
color.RGBA{R: 146, G: 193, B: 198, A: 255}, // links
|
||||
color.RGBA{R: 84, G: 211, B: 168, A: 255}, // mentions
|
||||
color.RGBA{R: 156, G: 186, B: 53, A: 255}, // hashtags
|
||||
},
|
||||
out,
|
||||
emojiMask,
|
||||
hlMask,
|
||||
totalCharsWritten,
|
||||
0,
|
||||
FONT_SIZE*lineNumber*12/10,
|
||||
|
||||
Reference in New Issue
Block a user