Compute glyphLayout after text rendering. Implement correct cursor
movement.
This commit is contained in:
parent
ccd258306e
commit
a5d1c4bee6
|
|
@ -37,6 +37,8 @@ func run(w *app.Window) error {
|
||||||
renderer := ui.New(ui.Theme{FontSize: 14}, shaper, logic.State())
|
renderer := ui.New(ui.Theme{FontSize: 14}, shaper, logic.State())
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
var elems []ui.Element
|
var elems []ui.Element
|
||||||
|
var curScale, newScale float32
|
||||||
|
|
||||||
log.Printf("run: starting frameReceiver")
|
log.Printf("run: starting frameReceiver")
|
||||||
go frameReceiver(w, &mu, &elems, logic.FrameChan())
|
go frameReceiver(w, &mu, &elems, logic.FrameChan())
|
||||||
log.Printf("run: starting logic.Run")
|
log.Printf("run: starting logic.Run")
|
||||||
|
|
@ -55,16 +57,13 @@ func run(w *app.Window) error {
|
||||||
case app.FrameEvent:
|
case app.FrameEvent:
|
||||||
log.Printf("run: FrameEvent")
|
log.Printf("run: FrameEvent")
|
||||||
gtx := app.NewContext(&ops, e)
|
gtx := app.NewContext(&ops, e)
|
||||||
newScale := gtx.Metric.PxPerDp
|
newScale = gtx.Metric.PxPerDp
|
||||||
curScale := logic.State().Scale()
|
curScale = logic.State().Scale()
|
||||||
if newScale != curScale {
|
|
||||||
logic.ConfigChan() <- editor.ScaleEvent{newScale}
|
|
||||||
}
|
|
||||||
// Deadlock risk: ensure no channel sends while lock is held
|
// Deadlock risk: ensure no channel sends while lock is held
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
currentElems := elems
|
currentElems := elems
|
||||||
renderer.Draw(gtx, currentElems)
|
renderer.Draw(gtx, currentElems)
|
||||||
lastLineY := int(renderer.LastLineY())
|
glyphLayout := renderer.GlyphLayout()
|
||||||
// Send search query update to the logic goroutine when it changes.
|
// Send search query update to the logic goroutine when it changes.
|
||||||
// The logic goroutine handles filtering and triggers a new frame.
|
// The logic goroutine handles filtering and triggers a new frame.
|
||||||
newQuery := logic.State().Browser.SearchEditor.Text()
|
newQuery := logic.State().Browser.SearchEditor.Text()
|
||||||
|
|
@ -112,13 +111,16 @@ func run(w *app.Window) error {
|
||||||
}
|
}
|
||||||
e.Frame(&ops)
|
e.Frame(&ops)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
if newScale != curScale {
|
||||||
|
logic.ConfigChan() <- editor.ScaleEvent{newScale}
|
||||||
|
}
|
||||||
if len(events) > 0 {
|
if len(events) > 0 {
|
||||||
logic.InputChan() <- events
|
logic.InputChan() <- events
|
||||||
}
|
}
|
||||||
if sendQuery {
|
if sendQuery {
|
||||||
logic.SearchQueryChan() <- newQuery
|
logic.SearchQueryChan() <- newQuery
|
||||||
}
|
}
|
||||||
logic.DisplayLineChan() <- lastLineY
|
logic.LayoutChan() <- glyphLayout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ func (r *Renderer) drawWrappedText(...) GlyphLayout {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`TextField.Draw` will store the returned layout on the renderer for the main loop to pick up.
|
`TextField.Draw` calls `drawWrappedText`, which captures the layout as a side effect and stores it on the renderer (`r.glyphLayout`). No code change is needed in `element.go` — the storage happens entirely inside `drawWrappedText` in `render.go`.
|
||||||
|
|
||||||
### 8.5 Feedback Path
|
### 8.5 Feedback Path
|
||||||
|
|
||||||
|
|
@ -280,7 +280,7 @@ func HandleCursorMove(delta int) {
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `internal/ui/unit.go` | No `GlyphLayout` type defined | Add `GlyphLayout` struct |
|
| `internal/ui/unit.go` | No `GlyphLayout` type defined | Add `GlyphLayout` struct |
|
||||||
| `internal/ui/render.go` | `drawWrappedText` tracks `lastLineY` and `displayLineCount` only | Return `GlyphLayout`; derive `lastLineY` from it; expose via `r.GlyphLayout()` |
|
| `internal/ui/render.go` | `drawWrappedText` tracks `lastLineY` and `displayLineCount` only | Return `GlyphLayout`; derive `lastLineY` from it; expose via `r.GlyphLayout()` |
|
||||||
| `internal/ui/element.go` | `TextField.Draw` calls `drawWrappedText` but ignores any layout return value | Store returned `GlyphLayout` on renderer |
|
| `internal/ui/element.go` | `TextField.Draw` calls `drawWrappedText` | **No change** — layout capture and storage happens entirely inside `drawWrappedText` in `render.go` |
|
||||||
| `internal/editor/logic.go` | Uses `lastLineYChan` for feedback | Add `layoutChan`; handle it in `Run()` loop; remove `lastLineYChan` |
|
| `internal/editor/logic.go` | Uses `lastLineYChan` for feedback | Add `layoutChan`; handle it in `Run()` loop; remove `lastLineYChan` |
|
||||||
| `internal/editor/state.go` | `EditorLayout` uses fixed-width approximation (`EditorFontSize * 0.6`) and `byteOffsetToLineCol`; `HandleCursorMove` is byte-based (±1) | `EditorState.GlyphLayout` field; `EditorLayout` uses it for cursor positioning; `HandleCursorMove` uses it for navigation |
|
| `internal/editor/state.go` | `EditorLayout` uses fixed-width approximation (`EditorFontSize * 0.6`) and `byteOffsetToLineCol`; `HandleCursorMove` is byte-based (±1) | `EditorState.GlyphLayout` field; `EditorLayout` uses it for cursor positioning; `HandleCursorMove` uses it for navigation |
|
||||||
| `cmd/pad/main.go` | Sends `int(renderer.LastLineY())` on `DisplayLineChan()` after each frame | Send `r.GlyphLayout()` on `layoutChan` after draw; remove `lastLineYChan` usage |
|
| `cmd/pad/main.go` | Sends `int(renderer.LastLineY())` on `DisplayLineChan()` after each frame | Send `r.GlyphLayout()` on `layoutChan` after draw; remove `lastLineYChan` usage |
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ type Logic struct {
|
||||||
configChan chan ConfigUpdate
|
configChan chan ConfigUpdate
|
||||||
frameChan chan []ui.Element
|
frameChan chan []ui.Element
|
||||||
inputChan chan []ui.InputEvent
|
inputChan chan []ui.InputEvent
|
||||||
lastLineYChan chan int // last line Y (Dp, sent as int) feedback from renderer
|
layoutChan chan ui.GlyphLayout // per-glyph layout feedback from renderer (replaces lastLineYChan)
|
||||||
resultChan chan ResultEvent
|
resultChan chan ResultEvent
|
||||||
searchQueryChan chan string // search text updates from main goroutine
|
searchQueryChan chan string // search text updates from main goroutine
|
||||||
openFileChan chan string // Added for asynchronous file loading
|
openFileChan chan string // Added for asynchronous file loading
|
||||||
|
|
@ -82,7 +82,7 @@ func NewLogic() *Logic {
|
||||||
configChan: make(chan ConfigUpdate),
|
configChan: make(chan ConfigUpdate),
|
||||||
frameChan: make(chan []ui.Element),
|
frameChan: make(chan []ui.Element),
|
||||||
inputChan: make(chan []ui.InputEvent),
|
inputChan: make(chan []ui.InputEvent),
|
||||||
lastLineYChan: make(chan int),
|
layoutChan: make(chan ui.GlyphLayout),
|
||||||
resultChan: make(chan ResultEvent),
|
resultChan: make(chan ResultEvent),
|
||||||
searchQueryChan: make(chan string),
|
searchQueryChan: make(chan string),
|
||||||
openFileChan: make(chan string), // Initialized
|
openFileChan: make(chan string), // Initialized
|
||||||
|
|
@ -110,9 +110,9 @@ func (l *Logic) InputChan() chan<- []ui.InputEvent {
|
||||||
return l.inputChan
|
return l.inputChan
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisplayLineChan returns the last line Y feedback channel.
|
// LayoutChan returns the glyph layout feedback channel.
|
||||||
func (l *Logic) DisplayLineChan() chan<- int {
|
func (l *Logic) LayoutChan() chan<- ui.GlyphLayout {
|
||||||
return l.lastLineYChan
|
return l.layoutChan
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultChan returns the result channel for the logic goroutine.
|
// ResultChan returns the result channel for the logic goroutine.
|
||||||
|
|
@ -148,9 +148,16 @@ func (l *Logic) Run() {
|
||||||
log.Printf("Logic: ConfigEvent")
|
log.Printf("Logic: ConfigEvent")
|
||||||
update.apply(l.state)
|
update.apply(l.state)
|
||||||
l.frameChan <- l.state.layout(l.browserManager)
|
l.frameChan <- l.state.layout(l.browserManager)
|
||||||
case y := <-l.lastLineYChan:
|
case layout := <-l.layoutChan:
|
||||||
if ui.Dp(y) != l.state.LastLineY {
|
// Store the full GlyphLayout on editor state.
|
||||||
l.state.LastLineY = ui.Dp(y)
|
// Derive LastLineY from it for scroll clamping.
|
||||||
|
l.state.Editor.GlyphLayout = layout
|
||||||
|
var derivedLastLineY ui.Dp
|
||||||
|
if len(layout.Y) > 0 {
|
||||||
|
derivedLastLineY = layout.Y[len(layout.Y)-1]
|
||||||
|
}
|
||||||
|
if derivedLastLineY != l.state.LastLineY {
|
||||||
|
l.state.LastLineY = derivedLastLineY
|
||||||
l.frameChan <- l.state.layout(l.browserManager)
|
l.frameChan <- l.state.layout(l.browserManager)
|
||||||
}
|
}
|
||||||
case events := <-l.inputChan:
|
case events := <-l.inputChan:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package editor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"pad/internal/browser"
|
"pad/internal/browser"
|
||||||
"pad/internal/ui"
|
"pad/internal/ui"
|
||||||
|
|
@ -45,6 +46,7 @@ const (
|
||||||
type EditorState struct {
|
type EditorState struct {
|
||||||
Buffer string // Document content
|
Buffer string // Document content
|
||||||
CursorPosition int // Byte offset
|
CursorPosition int // Byte offset
|
||||||
|
GlyphLayout ui.GlyphLayout // Per-glyph layout from renderer
|
||||||
SelectionStart int // -1 if no selection
|
SelectionStart int // -1 if no selection
|
||||||
SelectionEnd int // -1 if no selection
|
SelectionEnd int // -1 if no selection
|
||||||
Dirty bool // Needs save
|
Dirty bool // Needs save
|
||||||
|
|
@ -342,15 +344,35 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
|
||||||
// for Gio to deliver key events to this element.
|
// for Gio to deliver key events to this element.
|
||||||
editorElem.Focused = TheState.FocusedElementID == "editor_text"
|
editorElem.Focused = TheState.FocusedElementID == "editor_text"
|
||||||
|
|
||||||
// Convert byte offset (CursorPosition) to line/column for cursor rendering.
|
// Position cursor from GlyphLayout.
|
||||||
line, col := byteOffsetToLineCol(TheState.Editor.Buffer, TheState.Editor.CursorPosition)
|
layout := TheState.Editor.GlyphLayout
|
||||||
|
pos := TheState.Editor.CursorPosition
|
||||||
// Compute cursor X/Y in screen coordinates.
|
|
||||||
// We approximate character width as EditorFontSize * 0.6 for monospace.
|
|
||||||
charWidth := ui.Dp(float32(EditorFontSize) * 0.6)
|
|
||||||
lineHeight := EditorLineHeight()
|
lineHeight := EditorLineHeight()
|
||||||
cursorX := editorRegion.X + ui.Dp(col)*charWidth
|
|
||||||
cursorY := editorRegion.Y + ui.Dp(line)*lineHeight - TheState.ScrollOffset
|
var cursorX, cursorY ui.Dp
|
||||||
|
if len(layout.ByteOffsets) == 0 {
|
||||||
|
// Empty buffer — place cursor at the top-left of the editor region.
|
||||||
|
cursorX = editorRegion.X
|
||||||
|
cursorY = editorRegion.Y
|
||||||
|
} else {
|
||||||
|
// Binary search for the glyph at or after the cursor byte offset.
|
||||||
|
idx := sort.Search(len(layout.ByteOffsets), func(i int) bool {
|
||||||
|
return layout.ByteOffsets[i] >= pos
|
||||||
|
})
|
||||||
|
if idx < len(layout.ByteOffsets) {
|
||||||
|
// Cursor is at a glyph position.
|
||||||
|
cursorX = editorRegion.X + layout.X[idx]
|
||||||
|
cursorY = editorRegion.Y - TheState.ScrollOffset + layout.Y[idx] - EditorFontSize
|
||||||
|
} else {
|
||||||
|
// Cursor is at end of buffer — place after last glyph.
|
||||||
|
cursorX = editorRegion.X + layout.X[len(layout.X)-1] + layout.Advance[len(layout.Advance)-1]
|
||||||
|
cursorY = editorRegion.Y - TheState.ScrollOffset + layout.Y[len(layout.Y)-1] - EditorFontSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive line/col for bottom bar display (still using byte-based
|
||||||
|
// approximation until GlyphLayout-based line tracking is added).
|
||||||
|
line, col := byteOffsetToLineCol(TheState.Editor.Buffer, TheState.Editor.CursorPosition)
|
||||||
|
|
||||||
// Create the cursor element at the computed position
|
// Create the cursor element at the computed position
|
||||||
cursorElem := ui.NewCursor(
|
cursorElem := ui.NewCursor(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/gesture"
|
"gioui.org/gesture"
|
||||||
|
|
@ -60,8 +61,9 @@ type Renderer struct {
|
||||||
clicks map[string]clickReg
|
clicks map[string]clickReg
|
||||||
Keys map[string]keyReg // Exported Keys map
|
Keys map[string]keyReg // Exported Keys map
|
||||||
scrolls map[string]scrollReg
|
scrolls map[string]scrollReg
|
||||||
displayLineCount int // number of display lines from last drawWrappedText
|
displayLineCount int // number of display lines from last drawWrappedText
|
||||||
lastLineY Dp // last line baseline offset from text origin, in Dp
|
lastLineY Dp // last line baseline offset from text origin, in Dp (derived from GlyphLayout)
|
||||||
|
glyphLayout GlyphLayout // captured per-glyph layout from last drawWrappedText
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Renderer.
|
// New creates a new Renderer.
|
||||||
|
|
@ -229,6 +231,13 @@ func (r *Renderer) LastLineY() Dp {
|
||||||
return r.lastLineY
|
return r.lastLineY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GlyphLayout returns the glyph layout data captured during the last
|
||||||
|
// drawWrappedText call. Used by the logic goroutine to position the cursor
|
||||||
|
// and navigate by glyph instead of byte offset.
|
||||||
|
func (r *Renderer) GlyphLayout() GlyphLayout {
|
||||||
|
return r.glyphLayout
|
||||||
|
}
|
||||||
|
|
||||||
// clippableElement is implemented by elements that need their own clip region
|
// clippableElement is implemented by elements that need their own clip region
|
||||||
// around all their content and interaction registrations.
|
// around all their content and interaction registrations.
|
||||||
type clippableElement interface {
|
type clippableElement interface {
|
||||||
|
|
@ -472,12 +481,28 @@ func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, w
|
||||||
var glyphs [32]text.Glyph
|
var glyphs [32]text.Glyph
|
||||||
line := glyphs[:0]
|
line := glyphs[:0]
|
||||||
lineCount := 0
|
lineCount := 0
|
||||||
var lastGlyphY float32 // shaper Y value of last glyph drawn
|
|
||||||
|
// Capture per-glyph layout data for cursor positioning and navigation.
|
||||||
|
var layout GlyphLayout
|
||||||
|
byteOffset := 0
|
||||||
for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() {
|
for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() {
|
||||||
|
// Record layout data for this glyph.
|
||||||
|
// g.X is in fixed.Int26_6 — shift >> 6 for device pixels, divide by scale for Dp.
|
||||||
|
// g.Y is the baseline in device pixels.
|
||||||
|
layout.ByteOffsets = append(layout.ByteOffsets, byteOffset)
|
||||||
|
layout.X = append(layout.X, Dp(float32(g.X>>6)/r.scale.Scale()))
|
||||||
|
layout.Y = append(layout.Y, Dp(float32(g.Y)/r.scale.Scale()))
|
||||||
|
layout.Advance = append(layout.Advance, Dp(float32(g.Advance>>6)/r.scale.Scale()))
|
||||||
|
|
||||||
|
// Advance byteOffset by g.Runes.
|
||||||
|
for i := uint16(0); i < g.Runes; i++ {
|
||||||
|
_, sz := utf8.DecodeRuneInString(str[byteOffset:])
|
||||||
|
byteOffset += sz
|
||||||
|
}
|
||||||
|
|
||||||
line = append(line, g)
|
line = append(line, g)
|
||||||
if g.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 {
|
if g.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 {
|
||||||
r.drawLine(gtx, line, reg.X, y, col)
|
r.drawLine(gtx, line, reg.X, y, col)
|
||||||
lastGlyphY = float32(line[len(line)-1].Y)
|
|
||||||
line = line[:0]
|
line = line[:0]
|
||||||
if g.Flags&text.FlagLineBreak != 0 {
|
if g.Flags&text.FlagLineBreak != 0 {
|
||||||
lineCount++
|
lineCount++
|
||||||
|
|
@ -486,7 +511,6 @@ func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, w
|
||||||
}
|
}
|
||||||
if len(line) > 0 {
|
if len(line) > 0 {
|
||||||
r.drawLine(gtx, line, reg.X, y, col)
|
r.drawLine(gtx, line, reg.X, y, col)
|
||||||
lastGlyphY = float32(line[len(line)-1].Y)
|
|
||||||
lineCount++
|
lineCount++
|
||||||
}
|
}
|
||||||
call := m.Stop()
|
call := m.Stop()
|
||||||
|
|
@ -494,7 +518,11 @@ func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, w
|
||||||
|
|
||||||
textClip.Pop()
|
textClip.Pop()
|
||||||
r.displayLineCount = lineCount
|
r.displayLineCount = lineCount
|
||||||
r.lastLineY = Dp(lastGlyphY / r.scale.Scale()) // pixels → Dp
|
// Store captured layout; derive lastLineY from it.
|
||||||
|
r.glyphLayout = layout
|
||||||
|
if len(layout.Y) > 0 {
|
||||||
|
r.lastLineY = layout.Y[len(layout.Y)-1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Renderer) drawPng(gtx layout.Context, img image.Image, reg Region, width, height Dp) {
|
func (r *Renderer) drawPng(gtx layout.Context, img image.Image, reg Region, width, height Dp) {
|
||||||
|
|
|
||||||
|
|
@ -63,3 +63,15 @@ func FromDp(r Region, scale float32) RegionPx {
|
||||||
H: ToPx(r.H, scale),
|
H: ToPx(r.H, scale),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GlyphLayout holds per-glyph layout data captured during text shaping.
|
||||||
|
// Each index i represents one glyph (one rune in the source string).
|
||||||
|
// ByteOffsets[i] is the byte position in the buffer, (X[i], Y[i]) is the
|
||||||
|
// glyph's screen location in Dp (X relative to text region origin, Y is the
|
||||||
|
// shaper baseline), and Advance[i] is the glyph's width in Dp.
|
||||||
|
type GlyphLayout struct {
|
||||||
|
ByteOffsets []int // byte offset of each glyph in the buffer
|
||||||
|
X []Dp // screen X (Dp) of each glyph, relative to text region origin
|
||||||
|
Y []Dp // screen Y (Dp) baseline of each glyph (shaper value)
|
||||||
|
Advance []Dp // advance width (Dp) of each glyph
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user