From a6e1d3ae078b4a09a84662a98149ba1d1524ee99 Mon Sep 17 00:00:00 2001 From: akiyosi Date: Fri, 29 Apr 2022 21:40:00 +0900 Subject: [PATCH] Add support gui_widgets --- editor/extmarks.go | 52 ++++++++ editor/guiwidget.go | 163 ++++++++++++++++++++++++ editor/screen.go | 5 + editor/screen_test.go | 40 +++--- editor/tooltip.go | 15 +++ editor/window.go | 284 +++++++++++++++++++++++++++++++++++++++++- editor/workspace.go | 102 +++++++++++++-- 7 files changed, 628 insertions(+), 33 deletions(-) create mode 100644 editor/extmarks.go create mode 100644 editor/guiwidget.go diff --git a/editor/extmarks.go b/editor/extmarks.go new file mode 100644 index 00000000..94f1249e --- /dev/null +++ b/editor/extmarks.go @@ -0,0 +1,52 @@ +package editor + +import ( + "github.com/akiyosi/goneovim/util" + "github.com/neovim/go-client/nvim" +) + +// windowExtmarks is +// ["win_extmark", grid, win, ns_id, mark_id, row, col] +// Updates the position of an extmark which is currently visible in a +// window. Only emitted if the mark has the `ui_watched` attribute. +func (ws *Workspace) windowExtmarks(args []interface{}) { + for _, e := range args { + arg := e.([]interface{}) + grid := util.ReflectToInt(arg[0]) + winid := (arg[1]).(nvim.Window) + _ = util.ReflectToInt(arg[2]) + markid := util.ReflectToInt(arg[3]) + row := util.ReflectToInt(arg[4]) + col := util.ReflectToInt(arg[5]) + + win, ok := ws.screen.getWindow(grid) + if !ok { + return + } + + var gw *Guiwidget + ws.guiWidgets.Range(func(_, gITF interface{}) bool { + g := gITF.(*Guiwidget) + if g == nil { + return true + } + if markid == g.markID && g.winid == winid { + gw = g + return false + } + return true + }) + if gw == nil { + continue + } + + win.storeGuiwidget(gw.id, gw) + cell := win.content[row][col] + if cell != nil { + cell.decal = &Decal{ + exists: true, + markid: markid, + } + } + } +} diff --git a/editor/guiwidget.go b/editor/guiwidget.go new file mode 100644 index 00000000..b1ff3bf5 --- /dev/null +++ b/editor/guiwidget.go @@ -0,0 +1,163 @@ +package editor + +import ( + "fmt" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "strconv" + "time" + + "github.com/akiyosi/goneovim/util" + "github.com/neovim/go-client/nvim" + "github.com/therecipe/qt/core" +) + +type Guiwidget struct { + Tooltip + + raw string + data *core.QByteArray + mime string + width int + height int + id int + markID int + winid nvim.Window +} + +func initGuiwidget() *Guiwidget { + guiwidget := NewGuiwidget(nil, 0) + guiwidget.data = nil + guiwidget.width = 0 + guiwidget.height = 0 + guiwidget.mime = "" + guiwidget.id = 0 + guiwidget.ConnectPaintEvent(guiwidget.paint) + + return guiwidget +} + +// GuiWidgetPut pushes visual resource data to the front-end +func (w *Workspace) handleRPCGuiwidgetput(updates []interface{}) { + for _, update := range updates { + a := update.(map[string]interface{}) + + idITF, ok := a["id"] + if !ok { + continue + } + id := util.ReflectToInt(idITF) + + var g *Guiwidget + if g, ok = w.getGuiwidgetFromResID(id); !ok { + g = initGuiwidget() + w.storeGuiwidget(id, g) + g.s = w.screen + } + + g.id = id + + mime, ok := a["mime"] + if ok { + g.mime = mime.(string) + } + + data, ok := a["data"] + if ok { + s := data.(string) + + switch mime { + case "text/plain": + g.text = s + + case "image/svg", + "image/svg+xml", + "image/png", + "image/gif", + "image/jpeg", + "image/*": + g.raw = s + default: + } + } + } +} + +// GuiWidgetUpdateView sends a list of "placements". +// A placement associates an extmark with a resource id, and provides +// display options for a widget (width, height, mouse events etc.). +func (w *Workspace) handleRPCGuiwidgetview(updates []interface{}) { + var markid, resid, width, height int + for _, update := range updates { + a := update.(map[string]interface{}) + + buf, ok := a["buf"] + if !ok { + continue + } + + errChan := make(chan error, 60) + var err error + var outstr string + go func() { + outstr, err = w.nvim.CommandOutput( + fmt.Sprintf("echo bufwinid(%d)", util.ReflectToInt(buf)), + ) + errChan <- err + }() + select { + case <-errChan: + case <-time.After(40 * time.Millisecond): + } + + out, _ := strconv.Atoi(outstr) + winid := (nvim.Window)(out) + + widgets, ok := a["widgets"] + if !ok { + continue + } + for _, e := range widgets.([]interface{}) { + for k, ee := range e.([]interface{}) { + if k == 0 { + markid = util.ReflectToInt(ee) + } else if k == 1 { + resid = util.ReflectToInt(ee) + } else if k == 2 { + width = util.ReflectToInt(ee) + } else if k == 3 { + height = util.ReflectToInt(ee) + } + if k >= 4 { + } + } + + var g *Guiwidget + if g, ok = w.getGuiwidgetFromResID(resid); !ok { + g = initGuiwidget() + w.storeGuiwidget(resid, g) + g.s = w.screen + } + + g.winid = winid + g.markID = markid + g.width = width + g.height = height + switch g.mime { + case "text/plain": + baseFont := g.s.ws.font + g.font = initFontNew( + baseFont.fontNew.Family(), + float64(g.height*baseFont.height)*0.8, + 0, + 0, + ) + default: + } + + } + + } + +} diff --git a/editor/screen.go b/editor/screen.go index b850016e..c706e1c9 100644 --- a/editor/screen.go +++ b/editor/screen.go @@ -1240,6 +1240,8 @@ func (s *Screen) gridScroll(args []interface{}) { } func (s *Screen) update() { + // shownMarks := []int{} + s.windows.Range(func(grid, winITF interface{}) bool { win := winITF.(*Window) // if grid is dirty, we remove this grid @@ -1264,6 +1266,9 @@ func (s *Screen) update() { win.fill() } win.update() + + // shownMarksWin := win.updateExtMarks() + // shownMarks = append(shownMarks, shownMarksWin...) } return true diff --git a/editor/screen_test.go b/editor/screen_test.go index 80164734..902df94f 100644 --- a/editor/screen_test.go +++ b/editor/screen_test.go @@ -223,11 +223,11 @@ func TestWindow_updateLine(t *testing.T) { }, }, []Cell{ - Cell{hldef[7], "~", true}, - Cell{hldef[7], " ", true}, - Cell{hldef[7], " ", true}, - Cell{hldef[7], " ", true}, - Cell{hldef[7], " ", true}, + Cell{hldef[7], "~", true, nil}, + Cell{hldef[7], " ", true, nil}, + Cell{hldef[7], " ", true, nil}, + Cell{hldef[7], " ", true, nil}, + Cell{hldef[7], " ", true, nil}, }, }, { @@ -248,11 +248,11 @@ func TestWindow_updateLine(t *testing.T) { }, }, []Cell{ - Cell{hldef[7], "~", true}, - Cell{hldef[7], " ", true}, - Cell{hldef[7], " ", true}, - Cell{hldef[6], "*", true}, - Cell{hldef[6], "*", true}, + Cell{hldef[7], "~", true, nil}, + Cell{hldef[7], " ", true, nil}, + Cell{hldef[7], " ", true, nil}, + Cell{hldef[6], "*", true, nil}, + Cell{hldef[6], "*", true, nil}, }, }, { @@ -276,11 +276,11 @@ func TestWindow_updateLine(t *testing.T) { }, }, []Cell{ - Cell{hldef[7], "~", true}, - Cell{hldef[6], "@", true}, - Cell{hldef[6], "v", true}, - Cell{hldef[6], "i", true}, - Cell{hldef[6], "m", true}, + Cell{hldef[7], "~", true, nil}, + Cell{hldef[6], "@", true, nil}, + Cell{hldef[6], "v", true, nil}, + Cell{hldef[6], "i", true, nil}, + Cell{hldef[6], "m", true, nil}, }, }, { @@ -302,11 +302,11 @@ func TestWindow_updateLine(t *testing.T) { }, }, []Cell{ - Cell{hldef[7], " ", true}, - Cell{hldef[7], " ", true}, - Cell{hldef[7], "J", true}, - Cell{hldef[6], "i", true}, - Cell{hldef[6], "m", true}, + Cell{hldef[7], " ", true, nil}, + Cell{hldef[7], " ", true, nil}, + Cell{hldef[7], "J", true, nil}, + Cell{hldef[6], "i", true, nil}, + Cell{hldef[6], "m", true, nil}, }, }, } diff --git a/editor/tooltip.go b/editor/tooltip.go index 094e2167..1f991b70 100644 --- a/editor/tooltip.go +++ b/editor/tooltip.go @@ -59,6 +59,12 @@ func (t *Tooltip) drawForeground(p *gui.QPainter, f func(*gui.QPainter), g func( } func (t *Tooltip) setQpainterFont(p *gui.QPainter) { + if p == nil { + return + } + if t.font == nil { + return + } p.SetFont(t.font.fontNew) } @@ -131,11 +137,20 @@ func (t *Tooltip) update() { tooltipWidth += w } + font := t.font + if font == nil { + font = t.s.ws.font + } + // update widget size t.SetFixedSize2( int(tooltipWidth), t.font.lineHeight, ) + t.SetAutoFillBackground(true) + p := gui.NewQPalette() + p.SetColor2(gui.QPalette__Background, t.s.ws.background.QColor()) + t.SetPalette(p) t.Update() } diff --git a/editor/window.go b/editor/window.go index 94f73371..dd8f4639 100644 --- a/editor/window.go +++ b/editor/window.go @@ -18,6 +18,7 @@ import ( "github.com/neovim/go-client/nvim" "github.com/therecipe/qt/core" "github.com/therecipe/qt/gui" + "github.com/therecipe/qt/svg" "github.com/therecipe/qt/widgets" ) @@ -62,11 +63,18 @@ type HlDecoration struct { strikethrough bool } +// Decal is +type Decal struct { + exists bool + markid int +} + // Cell is type Cell struct { highlight *Highlight char string normalWidth bool + decal *Decal } type IntInt [2]int @@ -150,6 +158,8 @@ type Window struct { isMsgGrid bool isGridDirty bool doGetSnapshot bool + hasExtmarks bool + guiWidgets sync.Map } type localWindow struct { @@ -254,6 +264,10 @@ func (w *Window) paint(event *gui.QPaintEvent) { w.drawForeground(p, y, col, cols) } + for y := row; y < row+rows; y++ { + w.drawExtmarks(p, y, col, cols) + } + // Draw scroll snapshot // TODO: If there are wrapped lines in the viewport, the snapshot will be misaligned. w.drawScrollSnapshot(p) @@ -1213,6 +1227,9 @@ func (w *Window) updateLine(row, col int, cells []interface{}) { line[col].char = cell[0].(string) line[col].normalWidth = w.isNormalWidth(line[col].char) + if line[col].decal != nil { + line[col].decal = nil + } // If `hl_id` is not present the most recently seen `hl_id` in // the same call should be used (it is always sent for the first @@ -1289,7 +1306,33 @@ func (w *Window) countContent(row int) { !cell.highlight.underline && !cell.highlight.undercurl && !cell.highlight.strikethrough { - width-- + if cell.decal != nil { + if cell.decal.exists { + w.hasExtmarks = true + + // If an extmark exists, the following process is performed + // to allocate the corresponding area for updating. + w.s.ws.guiWidgets.Range(func(_, resITF interface{}) bool { + res := resITF.(*Guiwidget) + if res == nil { + return true + } + + if res.markID == cell.decal.markid { + width += res.width + breakFlag[1] = true + for k := 1; k <= res.height; k++ { + w.lenOldContent[k] = width + } + } + + return true + }) + + } + } else { + width-- + } } else { breakFlag[1] = true } @@ -1495,7 +1538,7 @@ func (w *Window) update() { start := w.queueRedrawArea[1] end := w.queueRedrawArea[3] // Update all lines when using the wheel scroll or indent guide feature. - if w.scrollPixels[1] != 0 || editor.config.Editor.IndentGuide || w.s.name == "minimap" { + if w.scrollPixels[1] != 0 || editor.config.Editor.IndentGuide || w.s.name == "minimap" || w.hasExtmarks { start = 0 end = w.rows } @@ -1522,7 +1565,7 @@ func (w *Window) update() { drawWithSingleRect := false // If DrawIndentGuide is enabled - if editor.config.Editor.IndentGuide { + if editor.config.Editor.IndentGuide || w.hasExtmarks { if i < w.rows-1 { if width < w.lenContent[i+1] { width = w.lenContent[i+1] @@ -2457,6 +2500,193 @@ func (w *Window) drawTextDecoration(p *gui.QPainter, y int, col int, cols int) { } } +func (w *Window) drawExtmarks(p *gui.QPainter, y int, col int, cols int) { + if y >= len(w.content) { + return + } + line := w.content[y] + font := w.getFont() + + // Set smooth scroll offset + scrollPixels := 0 + if w.lastScrollphase != core.Qt__NoScrollPhase { + scrollPixels = w.scrollPixels2 + } + if editor.config.Editor.LineToScroll == 1 { + scrollPixels += w.scrollPixels[1] + } + + for x := 0; x <= col+cols; x++ { + if x >= len(line) { + continue + } + if line[x] == nil { + continue + } + if line[x].decal == nil { + continue + } + if line[x].decal.exists { + + w.guiWidgets.Range(func(_, resITF interface{}) bool { + res := resITF.(*Guiwidget) + if res == nil { + return true + } + + if res.markID == line[x].decal.markid { + width := int(float64(res.width) * font.cellwidth) + height := res.height * font.lineHeight + + if res.mime == "text/plain" { + res.updateText(res.text) + p.FillRect5( + int(float64(x)*font.cellwidth), + y*font.lineHeight+scrollPixels, + width, + height, + w.s.ws.background.QColor(), + ) + } + res.drawExtmark( + int(float64(x)*font.cellwidth), + y*font.lineHeight+scrollPixels, + width, + height, + p, + res.setQpainterFont, + res.getNthWidthAndShift, + w.devicePixelRatio, + ) + } + + return true + }) + + } + + } +} + +func (g *Guiwidget) drawExtmark(x, y, width, height int, p *gui.QPainter, f func(*gui.QPainter), h func(int) (float64, int), devicePixelRatio float64) { + f(p) + + p.SetPen2(g.s.ws.foreground.QColor()) + + switch g.mime { + case "text/plain": + if g.text != "" { + r := []rune(g.text) + var pos float64 + for k := 0; k < len(r); k++ { + + width, shift := h(k) + pos += width + + p.DrawText( + core.NewQPointF3( + float64(x)+pos, + float64(y+shift), + ), + string(r[k]), + ) + + } + } + case "image/svg": + g.data = core.NewQByteArray2(g.raw, len(g.raw)) + + if g.data == nil { + return + } + renderer := svg.NewQSvgRenderer3( + g.data, + nil, + ) + + image := gui.NewQImage3( + g.width, + g.height, + gui.QImage__Format_ARGB32_Premultiplied, + ) + if image == nil { + return + } + image.SetDevicePixelRatio(devicePixelRatio) + image.Fill3(core.Qt__transparent) + pi := gui.NewQPainter2(image) + + renderer.Render(pi) + + g.drawImage(p, image, x, y, width, height) + + case "image/*", + "image/gif", + "image/jpeg", + "image/png": + g.data = core.NewQByteArray2(g.raw, len(g.raw)) + + image := gui.NewQImage3( + g.width, + g.height, + gui.QImage__Format_ARGB32_Premultiplied, + ) + image.SetDevicePixelRatio(devicePixelRatio) + // image.Fill2(g.s.ws.background.QColor()) + if g.data == nil { + return + } + result := image.LoadFromData2( + g.data, + g.mime, + ) + if !result { + return + } + + g.drawImage(p, image, x, y, width, height) + default: + } +} + +func (g *Guiwidget) drawImage(p *gui.QPainter, image *gui.QImage, x, y, width, height int) { + if image == nil { + return + } + var pixmap *gui.QPixmap + pixmap = pixmap.FromImage2( + image, + core.Qt__AutoColor, + ) + if pixmap == nil { + return + } + + pixmap = pixmap.ScaledToHeight( + height, + core.Qt__SmoothTransformation, + ) + + if pixmap == nil { + return + } + + p.FillRect5( + x, y, + pixmap.Rect().Width(), + height, + g.s.ws.background.QColor(), + ) + + p.DrawPixmap7( + core.NewQPointF3( + float64(x), + float64(y), + ), + pixmap, + ) +} + func (w *Window) getFillpatternAndTransparent(hl *Highlight) (core.Qt__BrushStyle, *RGBA, int) { color := hl.bg() pattern := core.Qt__BrushStyle(1) @@ -3189,3 +3419,51 @@ func (w *Window) layoutExternalWindow(x, y int) { } } + +func (w *Window) getGuiwidgetFromResID(resid int) (*Guiwidget, bool) { + gITF, ok := w.guiWidgets.Load(resid) + if !ok { + return nil, false + } + g := gITF.(*Guiwidget) + if g == nil { + return nil, false + } + + return g, true + // for _, g := range w.guiWidgets { + // if resid == g.id { + // return g + // } + // } + + // return nil +} + +func (w *Window) getGuiwidgetFromMarkID(markid int) *Guiwidget { + + // for _, g := range w.guiWidgets { + // if markid == g.markID { + // return g + // } + // } + + var guiwidget *Guiwidget + w.guiWidgets.Range(func(_, gITF interface{}) bool { + g := gITF.(*Guiwidget) + if g == nil { + return true + } + if markid == g.markID { + guiwidget = g + return true + } + return true + }) + + return guiwidget +} + +func (w *Window) storeGuiwidget(resid int, g *Guiwidget) { + w.guiWidgets.Store(resid, g) +} diff --git a/editor/workspace.go b/editor/workspace.go index abfdc6f1..e8d4f42b 100644 --- a/editor/workspace.go +++ b/editor/workspace.go @@ -30,6 +30,8 @@ type workspaceSignal struct { _ func() `signal:"stopSignal"` _ func() `signal:"redrawSignal"` _ func() `signal:"guiSignal"` + _ func() `signal:"guiwidgetputSignal"` + _ func() `signal:"guiwidgetviewSignal"` _ func() `signal:"statuslineSignal"` _ func() `signal:"lintSignal"` @@ -106,21 +108,29 @@ type Workspace struct { isDrawTabline bool terminalMode bool isMouseEnabled bool + + guiwidgetNSID int + guiwidgetputUpdates chan []interface{} + guiwidgetviewUpdates chan []interface{} + // guiWidgets []*Guiwidget + guiWidgets sync.Map } func newWorkspace(path string) (*Workspace, error) { editor.putLog("initialize workspace") w := &Workspace{ - stop: make(chan struct{}), - signal: NewWorkspaceSignal(nil), - redrawUpdates: make(chan [][]interface{}, 1000), - guiUpdates: make(chan []interface{}, 1000), - viewportQue: make(chan [5]int, 99), - foreground: newRGBA(255, 255, 255, 1), - background: newRGBA(0, 0, 0, 1), - special: newRGBA(255, 255, 255, 1), - windowsFt: make(map[nvim.Window]string), - windowsTs: make(map[nvim.Window]int), + stop: make(chan struct{}), + signal: NewWorkspaceSignal(nil), + redrawUpdates: make(chan [][]interface{}, 1000), + guiUpdates: make(chan []interface{}, 1000), + guiwidgetputUpdates: make(chan []interface{}, 1000), + guiwidgetviewUpdates: make(chan []interface{}, 1000), + viewportQue: make(chan [5]int, 99), + foreground: newRGBA(255, 255, 255, 1), + background: newRGBA(0, 0, 0, 1), + special: newRGBA(255, 255, 255, 1), + windowsFt: make(map[nvim.Window]string), + windowsTs: make(map[nvim.Window]int), } w.registerSignal() @@ -342,6 +352,16 @@ func (w *Workspace) registerSignal() { editor.putLog("Received GUI event from neovim") w.handleRPCGui(updates) }) + w.signal.ConnectGuiwidgetputSignal(func() { + updates := <-w.guiwidgetputUpdates + editor.putLog("Received GUI event from neovim") + w.handleRPCGuiwidgetput(updates) + }) + w.signal.ConnectGuiwidgetviewSignal(func() { + updates := <-w.guiwidgetviewUpdates + editor.putLog("Received GUI event from neovim") + w.handleRPCGuiwidgetview(updates) + }) w.signal.ConnectLazyDrawSignal(func() { if w.hasLazyUI { return @@ -533,6 +553,14 @@ func (w *Workspace) startNvim(path string) error { return err } + neovim.RegisterHandler("GuiWidgetPut", func(updates ...interface{}) { + w.guiwidgetputUpdates <- updates + w.signal.GuiwidgetputSignal() + }) + neovim.RegisterHandler("GuiWidgetUpdateView", func(updates ...interface{}) { + w.guiwidgetviewUpdates <- updates + w.signal.GuiwidgetviewSignal() + }) neovim.RegisterHandler("Gui", func(updates ...interface{}) { w.guiUpdates <- updates w.signal.GuiSignal() @@ -1497,6 +1525,9 @@ func (w *Workspace) handleRedraw(updates [][]interface{}) { case "win_viewport": w.windowViewport(args) + case "win_extmark": + w.windowExtmarks(args) + // Popupmenu Events case "popupmenu_show": if w.cmdline != nil { @@ -2030,8 +2061,25 @@ func (w *Workspace) handleRPCGui(updates []interface{}) { case "gonvim_enter": editor.putLog("vim enter") w.vimEnterProcess() + + chid := w.nvim.ChannelID() + err := w.nvim.Call("GuiWidgetClientAttach", nil, chid) + if err != nil { + fmt.Println("Error calling GuiWidgetClientAttach:", err) + } + namespaces, err := w.nvim.Namespaces() + if err != nil { + fmt.Println("Error calling nvim_get_namespace():", err) + } + ns, ok := namespaces["GuiWidget"] + if !ok { + fmt.Println("Failed to get the id according to GuiWidget namespace", err) + } + w.guiwidgetNSID = ns + case "gonvim_uienter": editor.putLog("ui enter") + case "gonvim_resize": width, height := editor.setWindowSize(updates[1].(string)) editor.window.Resize2(width, height) @@ -2891,6 +2939,40 @@ func (w *Workspace) toggleIndentguide() { go w.nvim.Command("doautocmd WinEnter") } +func (w *Workspace) getGuiwidgetFromResID(resid int) (*Guiwidget, bool) { + gITF, ok := w.guiWidgets.Load(resid) + if !ok { + return nil, false + } + g := gITF.(*Guiwidget) + if g == nil { + return nil, false + } + + return g, true +} + +func (w *Workspace) getGuiwidgetFromMarkID(markid int) *Guiwidget { + var guiwidget *Guiwidget + w.guiWidgets.Range(func(_, gITF interface{}) bool { + g := gITF.(*Guiwidget) + if g == nil { + return true + } + if markid == g.markID { + guiwidget = g + return true + } + return true + }) + + return guiwidget +} + +func (w *Workspace) storeGuiwidget(resid int, g *Guiwidget) { + w.guiWidgets.Store(resid, g) +} + // WorkspaceSide is type WorkspaceSide struct { widget *widgets.QWidget