Add support for ligatures, cursor shapes (and images) (#304)
This commit is contained in:
parent
c18b702b61
commit
765a781055
|
@ -1,2 +1,3 @@
|
||||||
.idea
|
.idea
|
||||||
|
.vscode
|
||||||
/darktile
|
/darktile
|
||||||
|
|
26
README.md
26
README.md
|
@ -8,12 +8,19 @@ Darktile is a GPU rendered terminal emulator designed for tiling window managers
|
||||||
|
|
||||||
- GPU rendering
|
- GPU rendering
|
||||||
- Unicode support
|
- Unicode support
|
||||||
|
- Variety of themes available (or build your own!)
|
||||||
- Compiled-in powerline font
|
- Compiled-in powerline font
|
||||||
- Configurable/customisable, supports custom themes, fonts etc.
|
- Works with your favourite monospaced TTF/OTF fonts
|
||||||
- Hints: Context-aware overlays e.g. hex colour viewer
|
- Font ligatures (turn it off if you're not a ligature fan)
|
||||||
|
- Hints: Context-aware overlays e.g. hex colour viewer, octal permission annotation
|
||||||
- Take screenshots with a single key-binding
|
- Take screenshots with a single key-binding
|
||||||
- Sixel support
|
- Sixels
|
||||||
- Transparency
|
- Window transparency (0-100%)
|
||||||
|
- Customisable cursor (most popular image formats supported)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="cursor.gif">
|
||||||
|
</p>
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -43,11 +50,14 @@ Darktile will use sensible defaults if no config/theme files are available. The
|
||||||
Found in the config directory (see above) inside `config.yaml`.
|
Found in the config directory (see above) inside `config.yaml`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
opacity: 1.0 # window opacity: 0.0 is fully transparent, 1.0 is fully opaque
|
opacity: 1.0 # Window opacity: 0.0 is fully transparent, 1.0 is fully opaque
|
||||||
font:
|
font:
|
||||||
family: "" # Find possible values for this by running 'darktile list-fonts'
|
family: "" # Font family. Find possible values for this by running 'darktile list-fonts'
|
||||||
size: 16
|
size: 16 # Font size
|
||||||
dpi: 72
|
dpi: 72 # DPI
|
||||||
|
ligatures: true # Enable font ligatures e.g. render '≡' instead of '==='
|
||||||
|
cursor:
|
||||||
|
image: "" # Path to an image to render as your cursor (defaults to standard rectangular cursor)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example Theme
|
### Example Theme
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
|
@ -3,6 +3,7 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -48,6 +49,8 @@ var rootCmd = &cobra.Command{
|
||||||
if _, err := conf.Save(); err != nil {
|
if _, err := conf.Save(); err != nil {
|
||||||
return fmt.Errorf("failed to write config file: %w", err)
|
return fmt.Errorf("failed to write config file: %w", err)
|
||||||
}
|
}
|
||||||
|
fmt.Println("Config written.")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var theme *termutil.Theme
|
var theme *termutil.Theme
|
||||||
|
@ -91,6 +94,16 @@ var rootCmd = &cobra.Command{
|
||||||
gui.WithFontSize(conf.Font.Size),
|
gui.WithFontSize(conf.Font.Size),
|
||||||
gui.WithFontFamily(conf.Font.Family),
|
gui.WithFontFamily(conf.Font.Family),
|
||||||
gui.WithOpacity(conf.Opacity),
|
gui.WithOpacity(conf.Opacity),
|
||||||
|
gui.WithLigatures(conf.Font.Ligatures),
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Cursor.Image != "" {
|
||||||
|
img, err := getImageFromFilePath(conf.Cursor.Image)
|
||||||
|
if err != nil {
|
||||||
|
startupErrors = append(startupErrors, err)
|
||||||
|
} else {
|
||||||
|
options = append(options, gui.WithCursorImage(img))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if screenshotAfterMS > 0 {
|
if screenshotAfterMS > 0 {
|
||||||
|
@ -118,6 +131,16 @@ var rootCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getImageFromFilePath(filePath string) (image.Image, error) {
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
image, _, err := image.Decode(f)
|
||||||
|
return image, err
|
||||||
|
}
|
||||||
|
|
||||||
func Execute() error {
|
func Execute() error {
|
||||||
rootCmd.Flags().BoolVar(&showVersion, "version", showVersion, "Show darktile version information and exit")
|
rootCmd.Flags().BoolVar(&showVersion, "version", showVersion, "Show darktile version information and exit")
|
||||||
rootCmd.Flags().BoolVar(&rewriteConfig, "rewrite-config", rewriteConfig, "Write the resultant config after parsing config files and merging with defauls back to the config file")
|
rootCmd.Flags().BoolVar(&rewriteConfig, "rewrite-config", rewriteConfig, "Write the resultant config after parsing config files and merging with defauls back to the config file")
|
||||||
|
|
|
@ -12,12 +12,18 @@ import (
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Opacity float64
|
Opacity float64
|
||||||
Font Font
|
Font Font
|
||||||
|
Cursor Cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
type Font struct {
|
type Font struct {
|
||||||
Family string
|
Family string
|
||||||
Size float64
|
Size float64
|
||||||
DPI float64
|
DPI float64
|
||||||
|
Ligatures bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cursor struct {
|
||||||
|
Image string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorFileNotFound struct {
|
type ErrorFileNotFound struct {
|
||||||
|
|
|
@ -11,9 +11,10 @@ import (
|
||||||
var defaultConfig = Config{
|
var defaultConfig = Config{
|
||||||
Opacity: 1.0,
|
Opacity: 1.0,
|
||||||
Font: Font{
|
Font: Font{
|
||||||
Family: "", // internally packed font will be loaded by default
|
Family: "", // internally packed font will be loaded by default
|
||||||
Size: 18.0,
|
Size: 18.0,
|
||||||
DPI: 72.0,
|
DPI: 72.0,
|
||||||
|
Ligatures: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,322 +1,17 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
"github.com/liamg/darktile/internal/app/darktile/gui/render"
|
||||||
"github.com/hajimehoshi/ebiten/v2/text"
|
|
||||||
"github.com/liamg/darktile/internal/app/darktile/termutil"
|
|
||||||
imagefont "golang.org/x/image/font"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Draw renders the terminal GUI to the ebtien window. Required to implement the ebiten interface.
|
// Draw renders the terminal GUI to the ebtien window. Required to implement the ebiten interface.
|
||||||
func (g *GUI) Draw(screen *ebiten.Image) {
|
func (g *GUI) Draw(screen *ebiten.Image) {
|
||||||
|
render.
|
||||||
tmp := ebiten.NewImage(g.size.X, g.size.Y)
|
New(screen, g.terminal, g.fontManager, g.popupMessages, g.opacity, g.enableLigatures, g.cursorImage).
|
||||||
|
Draw()
|
||||||
cellSize := g.fontManager.CharSize()
|
|
||||||
dotDepth := g.fontManager.DotDepth()
|
|
||||||
|
|
||||||
buffer := g.terminal.GetActiveBuffer()
|
|
||||||
|
|
||||||
regularFace := g.fontManager.RegularFontFace()
|
|
||||||
boldFace := g.fontManager.BoldFontFace()
|
|
||||||
italicFace := g.fontManager.ItalicFontFace()
|
|
||||||
boldItalicFace := g.fontManager.BoldItalicFontFace()
|
|
||||||
|
|
||||||
var useFace imagefont.Face
|
|
||||||
|
|
||||||
defBg := g.terminal.Theme().DefaultBackground()
|
|
||||||
defFg := g.terminal.Theme().DefaultForeground()
|
|
||||||
|
|
||||||
var colour color.Color
|
|
||||||
|
|
||||||
endX := float64(cellSize.X * int(buffer.ViewWidth()))
|
|
||||||
endY := float64(cellSize.Y * int(buffer.ViewHeight()))
|
|
||||||
extraW := float64(g.size.X) - endX
|
|
||||||
extraH := float64(g.size.Y) - endY
|
|
||||||
if extraW > 0 {
|
|
||||||
ebitenutil.DrawRect(tmp, endX, 0, extraW, endY, defBg)
|
|
||||||
}
|
|
||||||
if extraH > 0 {
|
|
||||||
ebitenutil.DrawRect(tmp, 0, endY, float64(g.size.X), extraH, defBg)
|
|
||||||
}
|
|
||||||
|
|
||||||
var inHighlight bool
|
|
||||||
var highlightRendered bool
|
|
||||||
var highlightMin termutil.Position
|
|
||||||
highlightMin.Col = uint16(g.size.X)
|
|
||||||
highlightMin.Line = uint64(g.size.Y)
|
|
||||||
var highlightMax termutil.Position
|
|
||||||
|
|
||||||
for y := int(buffer.ViewHeight() - 1); y >= 0; y-- {
|
|
||||||
py := cellSize.Y * y
|
|
||||||
|
|
||||||
ebitenutil.DrawRect(tmp, 0, float64(py), float64(g.size.X), float64(cellSize.Y), defBg)
|
|
||||||
inHighlight = false
|
|
||||||
for x := uint16(0); x < buffer.ViewWidth(); x++ {
|
|
||||||
cell := buffer.GetCell(x, uint16(y))
|
|
||||||
px := cellSize.X * int(x)
|
|
||||||
if cell != nil {
|
|
||||||
colour = cell.Bg()
|
|
||||||
} else {
|
|
||||||
colour = defBg
|
|
||||||
}
|
|
||||||
isCursor := g.terminal.GetActiveBuffer().IsCursorVisible() && int(buffer.CursorLine()) == y && buffer.CursorColumn() == x
|
|
||||||
if isCursor {
|
|
||||||
colour = g.terminal.Theme().CursorBackground()
|
|
||||||
} else if buffer.InSelection(termutil.Position{
|
|
||||||
Line: uint64(y),
|
|
||||||
Col: x,
|
|
||||||
}) {
|
|
||||||
colour = g.terminal.Theme().SelectionBackground()
|
|
||||||
} else if colour == nil {
|
|
||||||
colour = defBg
|
|
||||||
}
|
|
||||||
|
|
||||||
ebitenutil.DrawRect(tmp, float64(px), float64(py), float64(cellSize.X), float64(cellSize.Y), colour)
|
|
||||||
|
|
||||||
if buffer.IsHighlighted(termutil.Position{
|
|
||||||
Line: uint64(y),
|
|
||||||
Col: x,
|
|
||||||
}) {
|
|
||||||
|
|
||||||
if !inHighlight {
|
|
||||||
highlightRendered = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if uint64(y) < highlightMin.Line {
|
|
||||||
highlightMin.Col = uint16(g.size.X)
|
|
||||||
highlightMin.Line = uint64(y)
|
|
||||||
}
|
|
||||||
if uint64(y) > highlightMax.Line {
|
|
||||||
highlightMax.Line = uint64(y)
|
|
||||||
}
|
|
||||||
if uint64(y) == highlightMax.Line && x > highlightMax.Col {
|
|
||||||
highlightMax.Col = x
|
|
||||||
}
|
|
||||||
if uint64(y) == highlightMin.Line && x < highlightMin.Col {
|
|
||||||
highlightMin.Col = x
|
|
||||||
}
|
|
||||||
|
|
||||||
inHighlight = true
|
|
||||||
|
|
||||||
} else if inHighlight {
|
|
||||||
inHighlight = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if isCursor && !ebiten.IsFocused() {
|
|
||||||
ebitenutil.DrawRect(tmp, float64(px)+1, float64(py)+1, float64(cellSize.X)-2, float64(cellSize.Y)-2, g.terminal.Theme().DefaultBackground())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for x := uint16(0); x < buffer.ViewWidth(); x++ {
|
|
||||||
cell := buffer.GetCell(x, uint16(y))
|
|
||||||
if cell == nil || cell.Rune().Rune == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
px := cellSize.X * int(x)
|
|
||||||
colour = cell.Fg()
|
|
||||||
if g.terminal.GetActiveBuffer().IsCursorVisible() && int(buffer.CursorLine()) == y && buffer.CursorColumn() == x {
|
|
||||||
colour = g.terminal.Theme().CursorForeground()
|
|
||||||
} else if buffer.InSelection(termutil.Position{
|
|
||||||
Line: uint64(y),
|
|
||||||
Col: x,
|
|
||||||
}) {
|
|
||||||
colour = g.terminal.Theme().SelectionForeground()
|
|
||||||
} else if colour == nil {
|
|
||||||
colour = defFg
|
|
||||||
}
|
|
||||||
|
|
||||||
useFace = regularFace
|
|
||||||
if cell.Bold() && cell.Italic() {
|
|
||||||
useFace = boldItalicFace
|
|
||||||
} else if cell.Bold() {
|
|
||||||
useFace = boldFace
|
|
||||||
} else if cell.Italic() {
|
|
||||||
useFace = italicFace
|
|
||||||
}
|
|
||||||
|
|
||||||
if cell.Underline() {
|
|
||||||
uly := float64(py + (dotDepth+cellSize.Y)/2)
|
|
||||||
ebitenutil.DrawLine(tmp, float64(px), uly, float64(px+cellSize.X), uly, colour)
|
|
||||||
}
|
|
||||||
|
|
||||||
text.Draw(tmp, string(cell.Rune().Rune), useFace, px, py+dotDepth, colour)
|
|
||||||
|
|
||||||
if cell.Strikethrough() {
|
|
||||||
ebitenutil.DrawLine(tmp, float64(px), float64(py+(cellSize.Y/2)), float64(px+cellSize.X), float64(py+(cellSize.Y/2)), colour)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sixel := range buffer.GetVisibleSixels() {
|
|
||||||
sx := float64(int(sixel.Sixel.X) * cellSize.X)
|
|
||||||
sy := float64(sixel.ViewLineOffset * cellSize.Y)
|
|
||||||
|
|
||||||
op := &ebiten.DrawImageOptions{}
|
|
||||||
op.GeoM.Translate(sx, sy)
|
|
||||||
tmp.DrawImage(
|
|
||||||
ebiten.NewImageFromImage(sixel.Sixel.Image),
|
|
||||||
op,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw annotations and overlays
|
|
||||||
if highlightRendered {
|
|
||||||
if annotation := buffer.GetHighlightAnnotation(); annotation != nil {
|
|
||||||
|
|
||||||
if highlightMin.Col == uint16(g.size.X) {
|
|
||||||
highlightMin.Col = 0
|
|
||||||
}
|
|
||||||
if highlightMin.Line == uint64(g.size.Y) {
|
|
||||||
highlightMin.Line = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
mx, _ := ebiten.CursorPosition()
|
|
||||||
padding := float64(cellSize.X) / 2
|
|
||||||
lineX := float64(mx)
|
|
||||||
var lineY float64
|
|
||||||
var lineHeight float64
|
|
||||||
annotationX := mx - cellSize.X*2
|
|
||||||
var annotationY float64
|
|
||||||
annotationWidth := float64(cellSize.X) * annotation.Width
|
|
||||||
var annotationHeight float64
|
|
||||||
|
|
||||||
if annotationX+int(annotationWidth)+int(padding*2) > g.size.X {
|
|
||||||
annotationX = g.size.X - (int(annotationWidth) + int(padding*2))
|
|
||||||
}
|
|
||||||
if annotationX < int(padding) {
|
|
||||||
annotationX = int(padding)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (highlightMin.Line + (highlightMax.Line-highlightMin.Line)/2) < uint64(buffer.ViewHeight()/2) {
|
|
||||||
// annotate underneath max
|
|
||||||
|
|
||||||
pixelsUnderHighlight := float64(g.size.Y) - float64((highlightMax.Line+1)*uint64(cellSize.Y))
|
|
||||||
// we need to reserve at least one cell height for the label line
|
|
||||||
pixelsAvailableY := pixelsUnderHighlight - float64(cellSize.Y)
|
|
||||||
annotationHeight = annotation.Height * float64(cellSize.Y)
|
|
||||||
if annotationHeight > pixelsAvailableY {
|
|
||||||
annotationHeight = pixelsAvailableY
|
|
||||||
}
|
|
||||||
|
|
||||||
lineHeight = pixelsUnderHighlight - padding - annotationHeight
|
|
||||||
if lineHeight > annotationHeight {
|
|
||||||
if annotationHeight > float64(cellSize.Y)*3 {
|
|
||||||
lineHeight = annotationHeight
|
|
||||||
} else {
|
|
||||||
lineHeight = float64(cellSize.Y) * 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
annotationY = float64((highlightMax.Line+1)*uint64(cellSize.Y)) + lineHeight + float64(padding)
|
|
||||||
lineY = float64((highlightMax.Line + 1) * uint64(cellSize.Y))
|
|
||||||
|
|
||||||
} else {
|
|
||||||
//annotate above min
|
|
||||||
|
|
||||||
pixelsAboveHighlight := float64((highlightMin.Line) * uint64(cellSize.Y))
|
|
||||||
// we need to reserve at least one cell height for the label line
|
|
||||||
pixelsAvailableY := pixelsAboveHighlight - float64(cellSize.Y)
|
|
||||||
annotationHeight = annotation.Height * float64(cellSize.Y)
|
|
||||||
if annotationHeight > pixelsAvailableY {
|
|
||||||
annotationHeight = pixelsAvailableY
|
|
||||||
}
|
|
||||||
|
|
||||||
lineHeight = pixelsAboveHighlight - annotationHeight
|
|
||||||
if lineHeight > annotationHeight {
|
|
||||||
if annotationHeight > float64(cellSize.Y)*3 {
|
|
||||||
lineHeight = annotationHeight
|
|
||||||
} else {
|
|
||||||
lineHeight = float64(cellSize.Y) * 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
annotationY = float64((highlightMin.Line)*uint64(cellSize.Y)) - lineHeight - float64(padding*2) - annotationHeight
|
|
||||||
lineY = annotationY + annotationHeight + +padding
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw opaque box below and above highlighted line(s)
|
|
||||||
ebitenutil.DrawRect(tmp, 0, float64(highlightMin.Line*uint64(cellSize.Y)), float64(cellSize.X*int(highlightMin.Col)), float64(cellSize.Y), color.RGBA{A: 0x80})
|
|
||||||
ebitenutil.DrawRect(tmp, float64((cellSize.X)*int(highlightMax.Col+1)), float64(highlightMax.Line*uint64(cellSize.Y)), float64(g.size.X), float64(cellSize.Y), color.RGBA{A: 0x80})
|
|
||||||
ebitenutil.DrawRect(tmp, 0, 0, float64(g.size.X), float64(highlightMin.Line*uint64(cellSize.Y)), color.RGBA{A: 0x80})
|
|
||||||
afterLineY := float64((1 + highlightMax.Line) * uint64(cellSize.Y))
|
|
||||||
ebitenutil.DrawRect(tmp, 0, afterLineY, float64(g.size.X), float64(g.size.Y)-afterLineY, color.RGBA{A: 0x80})
|
|
||||||
|
|
||||||
// annotation border
|
|
||||||
ebitenutil.DrawRect(tmp, float64(annotationX)-padding, annotationY-padding, float64(annotationWidth)+(padding*2), annotationHeight+(padding*2), g.terminal.Theme().SelectionBackground())
|
|
||||||
// annotation background
|
|
||||||
ebitenutil.DrawRect(tmp, 1+float64(annotationX)-padding, 1+annotationY-padding, float64(annotationWidth)+(padding*2)-2, annotationHeight+(padding*2)-2, g.terminal.Theme().DefaultBackground())
|
|
||||||
|
|
||||||
// vertical line
|
|
||||||
ebitenutil.DrawLine(tmp, lineX, float64(lineY), lineX, lineY+lineHeight, g.terminal.Theme().SelectionBackground())
|
|
||||||
|
|
||||||
var tY int
|
|
||||||
var tX int
|
|
||||||
|
|
||||||
if annotation.Image != nil {
|
|
||||||
tY += annotation.Image.Bounds().Dy() + cellSize.Y/2
|
|
||||||
|
|
||||||
op := &ebiten.DrawImageOptions{}
|
|
||||||
op.GeoM.Translate(float64(annotationX), annotationY)
|
|
||||||
tmp.DrawImage(
|
|
||||||
ebiten.NewImageFromImage(annotation.Image),
|
|
||||||
op,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range annotation.Text {
|
|
||||||
if r == '\n' {
|
|
||||||
tY += cellSize.Y
|
|
||||||
tX = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
text.Draw(tmp, string(r), regularFace, annotationX+tX, int(annotationY)+dotDepth+tY, g.terminal.Theme().DefaultForeground())
|
|
||||||
tX += cellSize.X
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(g.popupMessages) > 0 {
|
|
||||||
pad := cellSize.Y / 2 // horizontal and vertical padding
|
|
||||||
msgEndY := endY
|
|
||||||
for _, msg := range g.popupMessages {
|
|
||||||
|
|
||||||
lines := strings.Split(msg.Text, "\n")
|
|
||||||
|
|
||||||
msgX := pad
|
|
||||||
|
|
||||||
msgY := msgEndY - float64(pad*3) - float64(cellSize.Y*len(lines))
|
|
||||||
|
|
||||||
msgText := msg.Text
|
|
||||||
|
|
||||||
boxWidth := float64(pad*2) + float64(cellSize.X*len(msgText))
|
|
||||||
boxHeight := float64(pad*2) + float64(cellSize.Y*len(lines))
|
|
||||||
|
|
||||||
if boxWidth < endX/8 {
|
|
||||||
boxWidth = endX / 8
|
|
||||||
}
|
|
||||||
|
|
||||||
ebitenutil.DrawRect(tmp, float64(msgX-1), msgY-1, boxWidth+2, boxHeight+2, msg.Foreground)
|
|
||||||
ebitenutil.DrawRect(tmp, float64(msgX), msgY, boxWidth, boxHeight, msg.Background)
|
|
||||||
for y, line := range lines {
|
|
||||||
for x, r := range line {
|
|
||||||
text.Draw(tmp, string(r), regularFace, msgX+pad+(x*cellSize.X), pad+(y*cellSize.Y)+int(msgY)+dotDepth, msg.Foreground)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
msgEndY = msgEndY - float64(pad*4) - float64(len(lines)*g.CellSize().Y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.screenshotRequested {
|
if g.screenshotRequested {
|
||||||
g.takeScreenshot(tmp)
|
g.takeScreenshot(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
opt := &ebiten.DrawImageOptions{}
|
|
||||||
opt.ColorM.Scale(1, 1, 1, g.opacity)
|
|
||||||
screen.DrawImage(tmp, opt)
|
|
||||||
tmp.Dispose()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,13 @@ package gui
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/liamg/darktile/internal/app/darktile/font"
|
"github.com/liamg/darktile/internal/app/darktile/font"
|
||||||
|
"github.com/liamg/darktile/internal/app/darktile/gui/popup"
|
||||||
"github.com/liamg/darktile/internal/app/darktile/hinters"
|
"github.com/liamg/darktile/internal/app/darktile/hinters"
|
||||||
"github.com/liamg/darktile/internal/app/darktile/termutil"
|
"github.com/liamg/darktile/internal/app/darktile/termutil"
|
||||||
|
|
||||||
|
@ -34,19 +34,14 @@ type GUI struct {
|
||||||
mousePos termutil.Position
|
mousePos termutil.Position
|
||||||
hinters []hinters.Hinter
|
hinters []hinters.Hinter
|
||||||
activeHinter int
|
activeHinter int
|
||||||
popupMessages []PopupMessage
|
popupMessages []popup.Message
|
||||||
screenshotRequested bool
|
screenshotRequested bool
|
||||||
screenshotFilename string
|
screenshotFilename string
|
||||||
startupFuncs []func(g *GUI)
|
startupFuncs []func(g *GUI)
|
||||||
keyState *keyState
|
keyState *keyState
|
||||||
opacity float64
|
opacity float64
|
||||||
}
|
enableLigatures bool
|
||||||
|
cursorImage *ebiten.Image
|
||||||
type PopupMessage struct {
|
|
||||||
Text string
|
|
||||||
Expiry time.Time
|
|
||||||
Foreground color.Color
|
|
||||||
Background color.Color
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MouseState uint8
|
type MouseState uint8
|
||||||
|
@ -59,12 +54,13 @@ const (
|
||||||
func New(terminal *termutil.Terminal, options ...Option) (*GUI, error) {
|
func New(terminal *termutil.Terminal, options ...Option) (*GUI, error) {
|
||||||
|
|
||||||
g := &GUI{
|
g := &GUI{
|
||||||
terminal: terminal,
|
terminal: terminal,
|
||||||
size: image.Point{80, 30},
|
size: image.Point{80, 30},
|
||||||
updateChan: make(chan struct{}),
|
updateChan: make(chan struct{}),
|
||||||
fontManager: font.NewManager(),
|
fontManager: font.NewManager(),
|
||||||
activeHinter: -1,
|
activeHinter: -1,
|
||||||
keyState: newKeyState(),
|
keyState: newKeyState(),
|
||||||
|
enableLigatures: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
|
|
|
@ -219,11 +219,7 @@ func (g *GUI) handleInput() error {
|
||||||
return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[6%s~", g.getModifierStr())))
|
return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[6%s~", g.getModifierStr())))
|
||||||
default:
|
default:
|
||||||
input := ebiten.AppendInputChars(nil)
|
input := ebiten.AppendInputChars(nil)
|
||||||
for _, runePressed := range input {
|
return g.terminal.WriteToPty([]byte(string(input)))
|
||||||
if err := g.terminal.WriteToPty([]byte(string(runePressed))); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
type Option func(g *GUI) error
|
type Option func(g *GUI) error
|
||||||
|
|
||||||
func WithFontFamily(family string) func(g *GUI) error {
|
func WithFontFamily(family string) func(g *GUI) error {
|
||||||
|
@ -29,6 +35,20 @@ func WithFontDPI(dpi float64) func(g *GUI) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithLigatures(enable bool) func(g *GUI) error {
|
||||||
|
return func(g *GUI) error {
|
||||||
|
g.enableLigatures = enable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCursorImage(img image.Image) func(g *GUI) error {
|
||||||
|
return func(g *GUI) error {
|
||||||
|
g.cursorImage = ebiten.NewImageFromImage(img)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithStartupFunc(f func(g *GUI)) Option {
|
func WithStartupFunc(f func(g *GUI)) Option {
|
||||||
return func(g *GUI) error {
|
return func(g *GUI) error {
|
||||||
g.startupFuncs = append(g.startupFuncs, f)
|
g.startupFuncs = append(g.startupFuncs, f)
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package popup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Text string
|
||||||
|
Expiry time.Time
|
||||||
|
Foreground color.Color
|
||||||
|
Background color.Color
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/liamg/darktile/internal/app/darktile/gui/popup"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -12,7 +14,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (g *GUI) ShowPopup(msg string, fg color.Color, bg color.Color, duration time.Duration) {
|
func (g *GUI) ShowPopup(msg string, fg color.Color, bg color.Color, duration time.Duration) {
|
||||||
g.popupMessages = append(g.popupMessages, PopupMessage{
|
g.popupMessages = append(g.popupMessages, popup.Message{
|
||||||
Text: msg,
|
Text: msg,
|
||||||
Expiry: time.Now().Add(duration),
|
Expiry: time.Now().Add(duration),
|
||||||
Foreground: fg,
|
Foreground: fg,
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Render) drawAnnotation() {
|
||||||
|
|
||||||
|
// 1. check if we have anything to highlight/annotate
|
||||||
|
highlightStart, highlightEnd, ok := r.buffer.GetViewHighlight()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. make everything outside of the highlighted area opaque
|
||||||
|
dimColour := color.RGBA{A: 0x80} // 50% alpha black overlay to dim non-highlighted area
|
||||||
|
for line := 0; line < int(r.buffer.ViewHeight()); line++ {
|
||||||
|
if line < int(highlightStart.Line) || line > int(highlightEnd.Line) {
|
||||||
|
ebitenutil.DrawRect(
|
||||||
|
r.frame,
|
||||||
|
0,
|
||||||
|
float64(line*r.font.CellSize.Y),
|
||||||
|
float64(r.pixelWidth),
|
||||||
|
float64(r.font.CellSize.Y),
|
||||||
|
dimColour, // 50% alpha black overlay to dim non-highlighted area
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if line == int(highlightStart.Line) && highlightStart.Col > 0 {
|
||||||
|
// we need to dim some content on this line before the highlight starts
|
||||||
|
ebitenutil.DrawRect(
|
||||||
|
r.frame,
|
||||||
|
0,
|
||||||
|
float64(line*r.font.CellSize.Y),
|
||||||
|
float64(int(highlightStart.Col)*r.font.CellSize.X),
|
||||||
|
float64(r.font.CellSize.Y),
|
||||||
|
dimColour,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if line == int(highlightEnd.Line) && highlightEnd.Col < r.buffer.ViewWidth()-2 {
|
||||||
|
// we need to dim some content on this line after the highlight ends
|
||||||
|
ebitenutil.DrawRect(
|
||||||
|
r.frame,
|
||||||
|
float64(int(highlightEnd.Col+1)*r.font.CellSize.X),
|
||||||
|
float64(line*r.font.CellSize.Y),
|
||||||
|
float64(int(r.buffer.ViewWidth()-(highlightEnd.Col+1))*r.font.CellSize.X),
|
||||||
|
float64(r.font.CellSize.Y),
|
||||||
|
dimColour,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. annotate the highlighted area (if there is an annotation)
|
||||||
|
annotation := r.buffer.GetHighlightAnnotation()
|
||||||
|
if annotation == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mousePixelX, _ := ebiten.CursorPosition()
|
||||||
|
padding := float64(r.font.CellSize.X) / 2
|
||||||
|
|
||||||
|
var lineY float64
|
||||||
|
var lineHeight float64
|
||||||
|
var annotationY float64
|
||||||
|
var annotationHeight float64
|
||||||
|
|
||||||
|
if (highlightStart.Line + (highlightEnd.Line-highlightStart.Line)/2) < uint64(r.buffer.ViewHeight()/2) {
|
||||||
|
// annotate underneath max
|
||||||
|
|
||||||
|
pixelsUnderHighlight := float64(r.pixelHeight) - float64((highlightEnd.Line+1)*uint64(r.font.CellSize.Y))
|
||||||
|
// we need to reserve at least one cell height for the label line
|
||||||
|
pixelsAvailableY := pixelsUnderHighlight - float64(r.font.CellSize.Y)
|
||||||
|
annotationHeight = annotation.Height * float64(r.font.CellSize.Y)
|
||||||
|
if annotationHeight > pixelsAvailableY {
|
||||||
|
annotationHeight = pixelsAvailableY
|
||||||
|
}
|
||||||
|
|
||||||
|
lineHeight = pixelsUnderHighlight - padding - annotationHeight
|
||||||
|
if lineHeight > annotationHeight {
|
||||||
|
if annotationHeight > float64(r.font.CellSize.Y)*3 {
|
||||||
|
lineHeight = annotationHeight
|
||||||
|
} else {
|
||||||
|
lineHeight = float64(r.font.CellSize.Y) * 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
annotationY = float64((highlightEnd.Line+1)*uint64(r.font.CellSize.Y)) + lineHeight + float64(padding)
|
||||||
|
lineY = float64((highlightEnd.Line + 1) * uint64(r.font.CellSize.Y))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//annotate above min
|
||||||
|
|
||||||
|
pixelsAboveHighlight := float64((highlightStart.Line) * uint64(r.font.CellSize.Y))
|
||||||
|
// we need to reserve at least one cell height for the label line
|
||||||
|
pixelsAvailableY := pixelsAboveHighlight - float64(r.font.CellSize.Y)
|
||||||
|
annotationHeight = annotation.Height * float64(r.font.CellSize.Y)
|
||||||
|
if annotationHeight > pixelsAvailableY {
|
||||||
|
annotationHeight = pixelsAvailableY
|
||||||
|
}
|
||||||
|
|
||||||
|
lineHeight = pixelsAboveHighlight - annotationHeight
|
||||||
|
if lineHeight > annotationHeight {
|
||||||
|
if annotationHeight > float64(r.font.CellSize.Y)*3 {
|
||||||
|
lineHeight = annotationHeight
|
||||||
|
} else {
|
||||||
|
lineHeight = float64(r.font.CellSize.Y) * 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
annotationY = float64((highlightStart.Line)*uint64(r.font.CellSize.Y)) - lineHeight - float64(padding*2) - annotationHeight
|
||||||
|
lineY = annotationY + annotationHeight + +padding
|
||||||
|
}
|
||||||
|
|
||||||
|
annotationX := mousePixelX - r.font.CellSize.X*2
|
||||||
|
annotationWidth := float64(r.font.CellSize.X) * annotation.Width
|
||||||
|
|
||||||
|
// if the annotation box goes off the right side of the terminal, align it against the right side
|
||||||
|
if annotationX+int(annotationWidth)+int(padding*2) > r.pixelWidth {
|
||||||
|
annotationX = r.pixelWidth - (int(annotationWidth) + int(padding*2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the annotation is too far left, align it against the left side
|
||||||
|
if annotationX < int(padding) {
|
||||||
|
annotationX = int(padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// annotation border
|
||||||
|
ebitenutil.DrawRect(r.frame, float64(annotationX)-padding, annotationY-padding, float64(annotationWidth)+(padding*2), annotationHeight+(padding*2), r.theme.SelectionBackground())
|
||||||
|
// annotation background
|
||||||
|
ebitenutil.DrawRect(r.frame, 1+float64(annotationX)-padding, 1+annotationY-padding, float64(annotationWidth)+(padding*2)-2, annotationHeight+(padding*2)-2, r.theme.DefaultBackground())
|
||||||
|
|
||||||
|
// vertical line
|
||||||
|
ebitenutil.DrawLine(r.frame, float64(mousePixelX), float64(lineY), float64(mousePixelX), lineY+lineHeight, r.theme.SelectionBackground())
|
||||||
|
|
||||||
|
var tY int
|
||||||
|
var tX int
|
||||||
|
|
||||||
|
if annotation.Image != nil {
|
||||||
|
tY += annotation.Image.Bounds().Dy() + r.font.CellSize.Y/2
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Translate(float64(annotationX), annotationY)
|
||||||
|
r.frame.DrawImage(
|
||||||
|
ebiten.NewImageFromImage(annotation.Image),
|
||||||
|
op,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ch := range annotation.Text {
|
||||||
|
if ch == '\n' {
|
||||||
|
tY += r.font.CellSize.Y
|
||||||
|
tX = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
text.Draw(r.frame, string(ch), r.font.Regular, annotationX+tX, int(annotationY)+r.font.DotDepth+tY, r.theme.DefaultForeground())
|
||||||
|
tX += r.font.CellSize.X
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
func (r *Render) drawContent() {
|
||||||
|
// draw base content for each row
|
||||||
|
defBg := r.theme.DefaultBackground()
|
||||||
|
defFg := r.theme.DefaultForeground()
|
||||||
|
for viewY := int(r.buffer.ViewHeight() - 1); viewY >= 0; viewY-- {
|
||||||
|
r.drawRow(viewY, defBg, defFg)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text"
|
||||||
|
"github.com/liamg/darktile/internal/app/darktile/termutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Render) drawCursor() {
|
||||||
|
//draw cursor
|
||||||
|
if !r.buffer.IsCursorVisible() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pixelX := float64(int(r.buffer.CursorColumn()) * r.font.CellSize.X)
|
||||||
|
pixelY := float64(int(r.buffer.CursorLine()) * r.font.CellSize.Y)
|
||||||
|
cell := r.buffer.GetCell(r.buffer.CursorColumn(), r.buffer.CursorLine())
|
||||||
|
|
||||||
|
useFace := r.font.Regular
|
||||||
|
if cell != nil {
|
||||||
|
if cell.Bold() && cell.Italic() {
|
||||||
|
useFace = r.font.BoldItalic
|
||||||
|
} else if cell.Bold() {
|
||||||
|
useFace = r.font.Bold
|
||||||
|
} else if cell.Italic() {
|
||||||
|
useFace = r.font.Italic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pixelW, pixelH := float64(r.font.CellSize.X), float64(r.font.CellSize.Y)
|
||||||
|
|
||||||
|
// empty rect without focus
|
||||||
|
if !ebiten.IsFocused() {
|
||||||
|
ebitenutil.DrawRect(r.frame, pixelX, pixelY, pixelW, pixelH, r.theme.CursorBackground())
|
||||||
|
ebitenutil.DrawRect(r.frame, pixelX+1, pixelY+1, pixelW-2, pixelH-2, r.theme.CursorForeground())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the cursor shape
|
||||||
|
switch r.buffer.GetCursorShape() {
|
||||||
|
case termutil.CursorShapeBlinkingBar, termutil.CursorShapeSteadyBar:
|
||||||
|
ebitenutil.DrawRect(r.frame, pixelX, pixelY, 2, pixelH, r.theme.CursorBackground())
|
||||||
|
case termutil.CursorShapeBlinkingUnderline, termutil.CursorShapeSteadyUnderline:
|
||||||
|
ebitenutil.DrawRect(r.frame, pixelX, pixelY+pixelH-2, pixelW, 2, r.theme.CursorBackground())
|
||||||
|
default:
|
||||||
|
// draw a custom cursor if we have one and there are no characters in the way
|
||||||
|
if r.cursorImage != nil && (cell == nil || cell.Rune().Rune == 0) {
|
||||||
|
opt := &ebiten.DrawImageOptions{}
|
||||||
|
_, h := r.cursorImage.Size()
|
||||||
|
ratio := 1 / (float64(h) / float64(r.font.CellSize.Y))
|
||||||
|
actualHeight := float64(h) * ratio
|
||||||
|
offsetY := (float64(r.font.CellSize.Y) - actualHeight) / 2
|
||||||
|
opt.GeoM.Scale(ratio, ratio)
|
||||||
|
opt.GeoM.Translate(pixelX, pixelY+offsetY)
|
||||||
|
r.frame.DrawImage(r.cursorImage, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ebitenutil.DrawRect(r.frame, pixelX, pixelY, pixelW, pixelH, r.theme.CursorBackground())
|
||||||
|
|
||||||
|
// we've drawn over the cell contents, so we need to draw it again in the cursor colours
|
||||||
|
if cell != nil && cell.Rune().Rune > 0 {
|
||||||
|
text.Draw(r.frame, string(cell.Rune().Rune), useFace, int(pixelX), int(pixelY)+r.font.DotDepth, r.theme.CursorForeground())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text"
|
||||||
|
imagefont "golang.org/x/image/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ligatures = map[string]rune{
|
||||||
|
":=": '≔',
|
||||||
|
"===": '≡',
|
||||||
|
"!=": '≠',
|
||||||
|
"!==": '≢',
|
||||||
|
"<=": '≤',
|
||||||
|
">=": '≥',
|
||||||
|
"=>": '⇒',
|
||||||
|
"->": '→',
|
||||||
|
"<-": '←',
|
||||||
|
"<>": '≷',
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Render) handleLigatures(sx uint16, sy uint16, face imagefont.Face, colour color.Color) (length int) {
|
||||||
|
|
||||||
|
var candidate string
|
||||||
|
for x := sx; x <= sx+2; x++ {
|
||||||
|
cell := r.buffer.GetCell(x, sy)
|
||||||
|
if cell == nil || cell.Rune().Rune == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
candidate += string(cell.Rune().Rune)
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(candidate) > 1 {
|
||||||
|
if ru, ok := ligatures[candidate]; ok {
|
||||||
|
// draw ligature
|
||||||
|
ligX := (int(sx) * r.font.CellSize.X) + (((len(candidate) - 1) * r.font.CellSize.X) / 2)
|
||||||
|
text.Draw(r.frame, string(ru), face, ligX, (int(sy)*r.font.CellSize.Y)+r.font.DotDepth, colour)
|
||||||
|
return len(candidate)
|
||||||
|
}
|
||||||
|
candidate = candidate[:len(candidate)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Render) drawPopups() {
|
||||||
|
|
||||||
|
if len(r.popups) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pad := r.font.CellSize.Y / 2 // horizontal and vertical padding
|
||||||
|
maxPixelX := float64(r.font.CellSize.X * int(r.buffer.ViewWidth()))
|
||||||
|
maxPixelY := float64(r.font.CellSize.Y * int(r.buffer.ViewHeight()))
|
||||||
|
|
||||||
|
for _, msg := range r.popups {
|
||||||
|
|
||||||
|
lines := strings.Split(msg.Text, "\n")
|
||||||
|
msgX := pad
|
||||||
|
msgY := maxPixelY - float64(pad*3) - float64(r.font.CellSize.Y*len(lines))
|
||||||
|
boxWidth := float64(pad*2) + float64(r.font.CellSize.X*len(msg.Text))
|
||||||
|
boxHeight := float64(pad*2) + float64(r.font.CellSize.Y*len(lines))
|
||||||
|
|
||||||
|
if boxWidth < maxPixelX/8 {
|
||||||
|
boxWidth = maxPixelX / 8
|
||||||
|
}
|
||||||
|
|
||||||
|
ebitenutil.DrawRect(r.frame, float64(msgX-1), msgY-1, boxWidth+2, boxHeight+2, msg.Foreground)
|
||||||
|
ebitenutil.DrawRect(r.frame, float64(msgX), msgY, boxWidth, boxHeight, msg.Background)
|
||||||
|
for y, line := range lines {
|
||||||
|
for x, c := range line {
|
||||||
|
text.Draw(r.frame, string(c), r.font.Regular, msgX+pad+(x*r.font.CellSize.X), pad+(y*r.font.CellSize.Y)+int(msgY)+r.font.DotDepth, msg.Foreground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maxPixelY = maxPixelY - float64(pad*4) - float64(len(lines)*r.font.CellSize.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/liamg/darktile/internal/app/darktile/font"
|
||||||
|
"github.com/liamg/darktile/internal/app/darktile/gui/popup"
|
||||||
|
"github.com/liamg/darktile/internal/app/darktile/termutil"
|
||||||
|
imagefont "golang.org/x/image/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Render struct {
|
||||||
|
frame *ebiten.Image
|
||||||
|
screen *ebiten.Image
|
||||||
|
terminal *termutil.Terminal
|
||||||
|
buffer *termutil.Buffer
|
||||||
|
theme *termutil.Theme
|
||||||
|
fontManager *font.Manager
|
||||||
|
pixelWidth int
|
||||||
|
pixelHeight int
|
||||||
|
font Font
|
||||||
|
opacity float64
|
||||||
|
popups []popup.Message
|
||||||
|
enableLigatures bool
|
||||||
|
cursorImage *ebiten.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
type Font struct {
|
||||||
|
Regular imagefont.Face
|
||||||
|
Bold imagefont.Face
|
||||||
|
Italic imagefont.Face
|
||||||
|
BoldItalic imagefont.Face
|
||||||
|
CellSize image.Point
|
||||||
|
DotDepth int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(screen *ebiten.Image, terminal *termutil.Terminal, fontManager *font.Manager, popups []popup.Message, opacity float64, enableLigatures bool, cursorImage *ebiten.Image) *Render {
|
||||||
|
w, h := screen.Size()
|
||||||
|
return &Render{
|
||||||
|
screen: screen,
|
||||||
|
frame: ebiten.NewImage(w, h),
|
||||||
|
terminal: terminal,
|
||||||
|
buffer: terminal.GetActiveBuffer(),
|
||||||
|
theme: terminal.Theme(),
|
||||||
|
fontManager: fontManager,
|
||||||
|
pixelWidth: w,
|
||||||
|
pixelHeight: h,
|
||||||
|
font: Font{
|
||||||
|
Regular: fontManager.RegularFontFace(),
|
||||||
|
Bold: fontManager.BoldFontFace(),
|
||||||
|
Italic: fontManager.ItalicFontFace(),
|
||||||
|
BoldItalic: fontManager.BoldItalicFontFace(),
|
||||||
|
CellSize: fontManager.CharSize(),
|
||||||
|
DotDepth: fontManager.DotDepth(),
|
||||||
|
},
|
||||||
|
opacity: opacity,
|
||||||
|
popups: popups,
|
||||||
|
enableLigatures: enableLigatures,
|
||||||
|
cursorImage: cursorImage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Render) Draw() {
|
||||||
|
|
||||||
|
// 1. fill frame with default background colour
|
||||||
|
r.frame.Fill(r.theme.DefaultBackground())
|
||||||
|
|
||||||
|
// 2. draw content (each row, each cell)
|
||||||
|
r.drawContent()
|
||||||
|
|
||||||
|
// 3. draw cursor
|
||||||
|
r.drawCursor()
|
||||||
|
|
||||||
|
// // 4. draw sixels
|
||||||
|
r.drawSixels()
|
||||||
|
|
||||||
|
// // 5. draw selection
|
||||||
|
r.drawSelection()
|
||||||
|
|
||||||
|
// // 6. draw highlight/annotations
|
||||||
|
r.drawAnnotation()
|
||||||
|
|
||||||
|
// // 7. draw popups
|
||||||
|
r.drawPopups()
|
||||||
|
|
||||||
|
// // 8. apply effects (e.g. transparency)
|
||||||
|
r.finalise()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Render) finalise() {
|
||||||
|
defer r.frame.Dispose()
|
||||||
|
opt := &ebiten.DrawImageOptions{}
|
||||||
|
opt.ColorM.Scale(1, 1, 1, r.opacity)
|
||||||
|
r.screen.DrawImage(r.frame, opt)
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text"
|
||||||
|
imagefont "golang.org/x/image/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Render) drawRow(viewY int, defaultBackgroundColour color.Color, defaultForegroundColour color.Color) {
|
||||||
|
|
||||||
|
pixelY := r.font.CellSize.Y * viewY
|
||||||
|
|
||||||
|
// draw a default colour background image across the entire row background
|
||||||
|
ebitenutil.DrawRect(r.frame, 0, float64(pixelY), float64(r.pixelWidth), float64(r.font.CellSize.Y), defaultBackgroundColour)
|
||||||
|
|
||||||
|
var colour color.Color
|
||||||
|
|
||||||
|
// draw background for each cell in row
|
||||||
|
for viewX := uint16(0); viewX < r.buffer.ViewWidth(); viewX++ {
|
||||||
|
cell := r.buffer.GetCell(viewX, uint16(viewY))
|
||||||
|
pixelX := r.font.CellSize.X * int(viewX)
|
||||||
|
if cell != nil {
|
||||||
|
colour = cell.Bg()
|
||||||
|
}
|
||||||
|
if colour == nil {
|
||||||
|
colour = defaultBackgroundColour
|
||||||
|
}
|
||||||
|
|
||||||
|
ebitenutil.DrawRect(r.frame, float64(pixelX), float64(pixelY), float64(r.font.CellSize.X), float64(r.font.CellSize.Y), colour)
|
||||||
|
}
|
||||||
|
|
||||||
|
var useFace imagefont.Face
|
||||||
|
var skipRunes int
|
||||||
|
|
||||||
|
// draw text content of each cell in row
|
||||||
|
for viewX := uint16(0); viewX < r.buffer.ViewWidth(); viewX++ {
|
||||||
|
|
||||||
|
cell := r.buffer.GetCell(viewX, uint16(viewY))
|
||||||
|
|
||||||
|
// we don't need to draw empty cells
|
||||||
|
if cell == nil || cell.Rune().Rune == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
colour = cell.Fg()
|
||||||
|
if colour == nil {
|
||||||
|
colour = defaultForegroundColour
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick a font face for the cell
|
||||||
|
if !cell.Bold() && !cell.Italic() {
|
||||||
|
useFace = r.font.Regular
|
||||||
|
} else if cell.Bold() && cell.Italic() {
|
||||||
|
useFace = r.font.Italic
|
||||||
|
} else if cell.Bold() {
|
||||||
|
useFace = r.font.Bold
|
||||||
|
} else if cell.Italic() {
|
||||||
|
useFace = r.font.Italic
|
||||||
|
}
|
||||||
|
|
||||||
|
pixelX := r.font.CellSize.X * int(viewX)
|
||||||
|
|
||||||
|
// underline the cell content if required
|
||||||
|
if cell.Underline() {
|
||||||
|
underlinePixelY := float64(pixelY + (r.font.DotDepth+r.font.CellSize.Y)/2)
|
||||||
|
ebitenutil.DrawLine(r.frame, float64(pixelX), underlinePixelY, float64(pixelX+r.font.CellSize.X), underlinePixelY, colour)
|
||||||
|
}
|
||||||
|
|
||||||
|
// strikethrough the cell if required
|
||||||
|
if cell.Strikethrough() {
|
||||||
|
ebitenutil.DrawLine(
|
||||||
|
r.frame,
|
||||||
|
float64(pixelX),
|
||||||
|
float64(pixelY+(r.font.CellSize.Y/2)),
|
||||||
|
float64(pixelX+r.font.CellSize.X),
|
||||||
|
float64(pixelY+(r.font.CellSize.Y/2)),
|
||||||
|
colour,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.enableLigatures && skipRunes == 0 {
|
||||||
|
skipRunes = r.handleLigatures(viewX, uint16(viewY), useFace, colour)
|
||||||
|
}
|
||||||
|
|
||||||
|
if skipRunes > 0 {
|
||||||
|
skipRunes--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the text for the cell
|
||||||
|
text.Draw(r.frame, string(cell.Rune().Rune), useFace, pixelX, pixelY+r.font.DotDepth, colour)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Render) drawSelection() {
|
||||||
|
_, selection := r.buffer.GetSelection()
|
||||||
|
if selection == nil {
|
||||||
|
// nothing selected
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bg, fg := r.theme.SelectionBackground(), r.theme.SelectionForeground()
|
||||||
|
|
||||||
|
for y := selection.Start.Line; y <= selection.End.Line; y++ {
|
||||||
|
xStart, xEnd := 0, int(r.buffer.ViewWidth())
|
||||||
|
if y == selection.Start.Line {
|
||||||
|
xStart = int(selection.Start.Col)
|
||||||
|
}
|
||||||
|
if y == selection.End.Line {
|
||||||
|
xEnd = int(selection.End.Col)
|
||||||
|
}
|
||||||
|
for x := xStart; x <= xEnd; x++ {
|
||||||
|
pX, pY := float64(x*r.font.CellSize.X), float64(y*uint64(r.font.CellSize.Y))
|
||||||
|
ebitenutil.DrawRect(r.frame, pX, pY, float64(r.font.CellSize.X), float64(r.font.CellSize.Y), bg)
|
||||||
|
cell := r.buffer.GetCell(uint16(x), uint16(y))
|
||||||
|
if cell == nil || cell.Rune().Rune == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
text.Draw(r.frame, string(cell.Rune().Rune), r.font.Regular, int(pX), int(pY)+r.font.DotDepth, fg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import "github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
func (r *Render) drawSixels() {
|
||||||
|
for _, sixel := range r.buffer.GetVisibleSixels() {
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Translate(
|
||||||
|
float64(int(sixel.Sixel.X)*r.font.CellSize.X),
|
||||||
|
float64(sixel.ViewLineOffset*r.font.CellSize.Y),
|
||||||
|
)
|
||||||
|
r.frame.DrawImage(
|
||||||
|
ebiten.NewImageFromImage(sixel.Sixel.Image),
|
||||||
|
op,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/liamg/darktile/internal/app/darktile/gui/popup"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (g *GUI) getModifierStr() string {
|
func (g *GUI) getModifierStr() string {
|
||||||
|
@ -40,7 +41,7 @@ func (g *GUI) Update() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GUI) filterPopupMessages() {
|
func (g *GUI) filterPopupMessages() {
|
||||||
var filtered []PopupMessage
|
var filtered []popup.Message
|
||||||
for _, msg := range g.popupMessages {
|
for _, msg := range g.popupMessages {
|
||||||
if time.Since(msg.Expiry) >= 0 {
|
if time.Since(msg.Expiry) >= 0 {
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -3,14 +3,28 @@ package termutil
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TabSize = 8
|
const TabSize = 8
|
||||||
|
|
||||||
|
type CursorShape uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
CursorShapeBlinkingBlock CursorShape = iota
|
||||||
|
CursorShapeDefault
|
||||||
|
CursorShapeSteadyBlock
|
||||||
|
CursorShapeBlinkingUnderline
|
||||||
|
CursorShapeSteadyUnderline
|
||||||
|
CursorShapeBlinkingBar
|
||||||
|
CursorShapeSteadyBar
|
||||||
|
)
|
||||||
|
|
||||||
type Buffer struct {
|
type Buffer struct {
|
||||||
lines []Line
|
lines []Line
|
||||||
savedCursorPos Position
|
savedCursorPos Position
|
||||||
savedCursorAttr *CellAttributes
|
savedCursorAttr *CellAttributes
|
||||||
|
cursorShape CursorShape
|
||||||
savedCharsets []*map[rune]rune
|
savedCharsets []*map[rune]rune
|
||||||
savedCurrentCharset int
|
savedCurrentCharset int
|
||||||
topMargin uint // see DECSTBM docs - this is for scrollable regions
|
topMargin uint // see DECSTBM docs - this is for scrollable regions
|
||||||
|
@ -31,6 +45,7 @@ type Buffer struct {
|
||||||
highlightEnd *Position
|
highlightEnd *Position
|
||||||
highlightAnnotation *Annotation
|
highlightAnnotation *Annotation
|
||||||
sixels []Sixel
|
sixels []Sixel
|
||||||
|
selectionMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type Annotation struct {
|
type Annotation struct {
|
||||||
|
@ -70,10 +85,19 @@ func NewBuffer(width, height uint16, maxLines uint64, fg color.Color, bg color.C
|
||||||
ShowCursor: true,
|
ShowCursor: true,
|
||||||
SixelScrolling: true,
|
SixelScrolling: true,
|
||||||
},
|
},
|
||||||
|
cursorShape: CursorShapeDefault,
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (buffer *Buffer) SetCursorShape(shape CursorShape) {
|
||||||
|
buffer.cursorShape = shape
|
||||||
|
}
|
||||||
|
|
||||||
|
func (buffer *Buffer) GetCursorShape() CursorShape {
|
||||||
|
return buffer.cursorShape
|
||||||
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) IsCursorVisible() bool {
|
func (buffer *Buffer) IsCursorVisible() bool {
|
||||||
return buffer.modes.ShowCursor
|
return buffer.modes.ShowCursor
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,13 +45,6 @@ func (t *Terminal) handleCSI(readChan chan MeasuredRune) (renderRequired bool) {
|
||||||
|
|
||||||
t.log("CSI P(%q) I(%q) %c", strings.Join(params, ";"), string(intermediate), final)
|
t.log("CSI P(%q) I(%q) %c", strings.Join(params, ";"), string(intermediate), final)
|
||||||
|
|
||||||
for _, b := range intermediate {
|
|
||||||
t.processRunes(MeasuredRune{
|
|
||||||
Rune: b,
|
|
||||||
Width: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
switch final {
|
switch final {
|
||||||
case 'c':
|
case 'c':
|
||||||
return t.csiSendDeviceAttributesHandler(params)
|
return t.csiSendDeviceAttributesHandler(params)
|
||||||
|
@ -73,6 +66,10 @@ func (t *Terminal) handleCSI(readChan chan MeasuredRune) (renderRequired bool) {
|
||||||
return t.csiSetMarginsHandler(params)
|
return t.csiSetMarginsHandler(params)
|
||||||
case 't':
|
case 't':
|
||||||
return t.csiWindowManipulation(params)
|
return t.csiWindowManipulation(params)
|
||||||
|
case 'q':
|
||||||
|
if string(intermediate) == " " {
|
||||||
|
return t.csiCursorSelection(params)
|
||||||
|
}
|
||||||
case 'A':
|
case 'A':
|
||||||
return t.csiCursorUpHandler(params)
|
return t.csiCursorUpHandler(params)
|
||||||
case 'B':
|
case 'B':
|
||||||
|
@ -112,15 +109,22 @@ func (t *Terminal) handleCSI(readChan chan MeasuredRune) (renderRequired bool) {
|
||||||
return t.csiSoftResetHandler(params)
|
return t.csiSoftResetHandler(params)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
default:
|
|
||||||
// TODO review this:
|
|
||||||
// if this is an unknown CSI sequence, write it to stdout as we can't handle it?
|
|
||||||
//_ = t.writeToRealStdOut(append([]rune{0x1b, '['}, raw...)...)
|
|
||||||
_ = raw
|
|
||||||
t.log("UNKNOWN CSI P(%s) I(%s) %c", strings.Join(params, ";"), string(intermediate), final)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, b := range intermediate {
|
||||||
|
t.processRunes(MeasuredRune{
|
||||||
|
Rune: b,
|
||||||
|
Width: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO review this:
|
||||||
|
// if this is an unknown CSI sequence, write it to stdout as we can't handle it?
|
||||||
|
//_ = t.writeToRealStdOut(append([]rune{0x1b, '['}, raw...)...)
|
||||||
|
_ = raw
|
||||||
|
t.log("UNKNOWN CSI P(%s) I(%s) %c", strings.Join(params, ";"), string(intermediate), final)
|
||||||
|
return false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WindowState uint8
|
type WindowState uint8
|
||||||
|
@ -963,6 +967,12 @@ func (t *Terminal) sgrSequenceHandler(params []string) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x := t.GetActiveBuffer().CursorColumn()
|
||||||
|
y := t.GetActiveBuffer().CursorLine()
|
||||||
|
if cell := t.GetActiveBuffer().GetCell(x, y); cell != nil {
|
||||||
|
cell.attr = t.GetActiveBuffer().cursorAttr
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -970,3 +980,15 @@ func (t *Terminal) csiSoftResetHandler(params []string) bool {
|
||||||
t.reset()
|
t.reset()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) csiCursorSelection(params []string) (renderRequired bool) {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i, err := strconv.Atoi(params[0])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t.GetActiveBuffer().SetCursorShape(CursorShape(i))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,10 @@ type Option func(t *Terminal)
|
||||||
|
|
||||||
func WithLogFile(path string) Option {
|
func WithLogFile(path string) Option {
|
||||||
return func(t *Terminal) {
|
return func(t *Terminal) {
|
||||||
|
if path == "-" {
|
||||||
|
t.logFile = os.Stdout
|
||||||
|
return
|
||||||
|
}
|
||||||
t.logFile, _ = os.Create(path)
|
t.logFile, _ = os.Create(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package termutil
|
package termutil
|
||||||
|
|
||||||
func (buffer *Buffer) ClearSelection() {
|
func (buffer *Buffer) ClearSelection() {
|
||||||
|
buffer.selectionMu.Lock()
|
||||||
|
defer buffer.selectionMu.Unlock()
|
||||||
buffer.selectionStart = nil
|
buffer.selectionStart = nil
|
||||||
buffer.selectionEnd = nil
|
buffer.selectionEnd = nil
|
||||||
}
|
}
|
||||||
|
@ -13,6 +15,9 @@ func (buffer *Buffer) GetBoundedTextAtPosition(pos Position) (start Position, en
|
||||||
|
|
||||||
// if the selection is invalid - e.g. lines are selected that no longer exist in the buffer
|
// if the selection is invalid - e.g. lines are selected that no longer exist in the buffer
|
||||||
func (buffer *Buffer) fixSelection() bool {
|
func (buffer *Buffer) fixSelection() bool {
|
||||||
|
buffer.selectionMu.Lock()
|
||||||
|
defer buffer.selectionMu.Unlock()
|
||||||
|
|
||||||
if buffer.selectionStart == nil || buffer.selectionEnd == nil {
|
if buffer.selectionStart == nil || buffer.selectionEnd == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -44,6 +49,9 @@ func (buffer *Buffer) ExtendSelectionToEntireLines() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buffer.selectionMu.Lock()
|
||||||
|
defer buffer.selectionMu.Unlock()
|
||||||
|
|
||||||
buffer.selectionStart.Col = 0
|
buffer.selectionStart.Col = 0
|
||||||
buffer.selectionEnd.Col = uint16(len(buffer.lines[buffer.selectionEnd.Line].cells)) - 1
|
buffer.selectionEnd.Col = uint16(len(buffer.lines[buffer.selectionEnd.Line].cells)) - 1
|
||||||
}
|
}
|
||||||
|
@ -150,6 +158,8 @@ FORWARD:
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) SetSelectionStart(pos Position) {
|
func (buffer *Buffer) SetSelectionStart(pos Position) {
|
||||||
|
buffer.selectionMu.Lock()
|
||||||
|
defer buffer.selectionMu.Unlock()
|
||||||
buffer.selectionStart = &Position{
|
buffer.selectionStart = &Position{
|
||||||
Col: pos.Col,
|
Col: pos.Col,
|
||||||
Line: buffer.convertViewLineToRawLine(uint16(pos.Line)),
|
Line: buffer.convertViewLineToRawLine(uint16(pos.Line)),
|
||||||
|
@ -157,10 +167,14 @@ func (buffer *Buffer) SetSelectionStart(pos Position) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) setRawSelectionStart(pos Position) {
|
func (buffer *Buffer) setRawSelectionStart(pos Position) {
|
||||||
|
buffer.selectionMu.Lock()
|
||||||
|
defer buffer.selectionMu.Unlock()
|
||||||
buffer.selectionStart = &pos
|
buffer.selectionStart = &pos
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) SetSelectionEnd(pos Position) {
|
func (buffer *Buffer) SetSelectionEnd(pos Position) {
|
||||||
|
buffer.selectionMu.Lock()
|
||||||
|
defer buffer.selectionMu.Unlock()
|
||||||
buffer.selectionEnd = &Position{
|
buffer.selectionEnd = &Position{
|
||||||
Col: pos.Col,
|
Col: pos.Col,
|
||||||
Line: buffer.convertViewLineToRawLine(uint16(pos.Line)),
|
Line: buffer.convertViewLineToRawLine(uint16(pos.Line)),
|
||||||
|
@ -168,6 +182,8 @@ func (buffer *Buffer) SetSelectionEnd(pos Position) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) setRawSelectionEnd(pos Position) {
|
func (buffer *Buffer) setRawSelectionEnd(pos Position) {
|
||||||
|
buffer.selectionMu.Lock()
|
||||||
|
defer buffer.selectionMu.Unlock()
|
||||||
buffer.selectionEnd = &pos
|
buffer.selectionEnd = &pos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +192,9 @@ func (buffer *Buffer) GetSelection() (string, *Selection) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buffer.selectionMu.Lock()
|
||||||
|
defer buffer.selectionMu.Unlock()
|
||||||
|
|
||||||
start := *buffer.selectionStart
|
start := *buffer.selectionStart
|
||||||
end := *buffer.selectionEnd
|
end := *buffer.selectionEnd
|
||||||
|
|
||||||
|
@ -187,6 +206,9 @@ func (buffer *Buffer) GetSelection() (string, *Selection) {
|
||||||
|
|
||||||
var text string
|
var text string
|
||||||
for y := start.Line; y <= end.Line; y++ {
|
for y := start.Line; y <= end.Line; y++ {
|
||||||
|
if y >= uint64(len(buffer.lines)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
line := buffer.lines[y]
|
line := buffer.lines[y]
|
||||||
startX := 0
|
startX := 0
|
||||||
endX := len(line.cells) - 1
|
endX := len(line.cells) - 1
|
||||||
|
@ -200,7 +222,13 @@ func (buffer *Buffer) GetSelection() (string, *Selection) {
|
||||||
text += "\n"
|
text += "\n"
|
||||||
}
|
}
|
||||||
for x := startX; x <= endX; x++ {
|
for x := startX; x <= endX; x++ {
|
||||||
|
if x >= len(line.cells) {
|
||||||
|
break
|
||||||
|
}
|
||||||
mr := line.cells[x].Rune()
|
mr := line.cells[x].Rune()
|
||||||
|
if mr.Width == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
x += mr.Width - 1
|
x += mr.Width - 1
|
||||||
text += string(mr.Rune)
|
text += string(mr.Rune)
|
||||||
}
|
}
|
||||||
|
@ -221,6 +249,8 @@ func (buffer *Buffer) InSelection(pos Position) bool {
|
||||||
if !buffer.fixSelection() {
|
if !buffer.fixSelection() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
buffer.selectionMu.Lock()
|
||||||
|
defer buffer.selectionMu.Unlock()
|
||||||
|
|
||||||
start := *buffer.selectionStart
|
start := *buffer.selectionStart
|
||||||
end := *buffer.selectionEnd
|
end := *buffer.selectionEnd
|
||||||
|
@ -256,31 +286,30 @@ func (buffer *Buffer) GetHighlightAnnotation() *Annotation {
|
||||||
return buffer.highlightAnnotation
|
return buffer.highlightAnnotation
|
||||||
}
|
}
|
||||||
|
|
||||||
// takes view coords
|
func (buffer *Buffer) GetViewHighlight() (start Position, end Position, exists bool) {
|
||||||
func (buffer *Buffer) IsHighlighted(pos Position) bool {
|
|
||||||
|
|
||||||
if buffer.highlightStart == nil || buffer.highlightEnd == nil {
|
if buffer.highlightStart == nil || buffer.highlightEnd == nil {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer.highlightStart.Line >= uint64(len(buffer.lines)) {
|
if buffer.highlightStart.Line >= uint64(len(buffer.lines)) {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer.highlightEnd.Line >= uint64(len(buffer.lines)) {
|
if buffer.highlightEnd.Line >= uint64(len(buffer.lines)) {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer.highlightStart.Col >= uint16(len(buffer.lines[buffer.highlightStart.Line].cells)) {
|
if buffer.highlightStart.Col >= uint16(len(buffer.lines[buffer.highlightStart.Line].cells)) {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer.highlightEnd.Col >= uint16(len(buffer.lines[buffer.highlightEnd.Line].cells)) {
|
if buffer.highlightEnd.Col >= uint16(len(buffer.lines[buffer.highlightEnd.Line].cells)) {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
start := *buffer.highlightStart
|
start = *buffer.highlightStart
|
||||||
end := *buffer.highlightEnd
|
end = *buffer.highlightEnd
|
||||||
|
|
||||||
if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) {
|
if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) {
|
||||||
swap := end
|
swap := end
|
||||||
|
@ -288,23 +317,8 @@ func (buffer *Buffer) IsHighlighted(pos Position) bool {
|
||||||
start = swap
|
start = swap
|
||||||
}
|
}
|
||||||
|
|
||||||
rY := buffer.convertViewLineToRawLine(uint16(pos.Line))
|
start.Line = uint64(buffer.convertRawLineToViewLine(start.Line))
|
||||||
if rY < start.Line {
|
end.Line = uint64(buffer.convertRawLineToViewLine(end.Line))
|
||||||
return false
|
|
||||||
}
|
|
||||||
if rY > end.Line {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if rY == start.Line {
|
|
||||||
if pos.Col < start.Col {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rY == end.Line {
|
|
||||||
if pos.Col > end.Col {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return start, end, true
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue