fix(editor): fix cursor positioning, scrolling, and clipping

- Fix cursor vertical alignment and ensure it respects ScrollOffset.
- Fix scrolling in editor page by correctly registering scroll interactions in the renderer.
- Ensure cursor is clipped to the editor text area to prevent drawing over status bars.
This commit is contained in:
Greg Pomerantz 2026-06-03 19:19:14 -04:00
parent a5d1c4bee6
commit d33e68ab2c
3 changed files with 29 additions and 1 deletions

View File

@ -374,13 +374,15 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
// approximation until GlyphLayout-based line tracking is added). // approximation until GlyphLayout-based line tracking is added).
line, col := byteOffsetToLineCol(TheState.Editor.Buffer, TheState.Editor.CursorPosition) 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.
// Ensure the cursor element knows its bounds for clipping.
cursorElem := ui.NewCursor( cursorElem := ui.NewCursor(
"editor_cursor", "editor_cursor",
ui.Region{X: cursorX, Y: cursorY, W: ui.Dp(2), H: lineHeight}, ui.Region{X: cursorX, Y: cursorY, W: ui.Dp(2), H: lineHeight},
line, col, line, col,
true, true,
) )
cursorElem.ClipRegion = editorRegion
return []ui.Element{statusBar, editorElem, cursorElem, bottomBar} return []ui.Element{statusBar, editorElem, cursorElem, bottomBar}
} }

View File

@ -480,6 +480,7 @@ func NewSearchBar(region Region, query string, match, total int, forward bool) S
type Cursor struct { type Cursor struct {
id string id string
region Region region Region
ClipRegion Region // Region to clip drawing
visible bool visible bool
interactions []Interaction interactions []Interaction
Line int Line int
@ -488,6 +489,7 @@ type Cursor struct {
Selection *Selection Selection *Selection
} }
func (c Cursor) NeedsClip() bool { return true }
func (c Cursor) Type() string { return "cursor" } func (c Cursor) Type() string { return "cursor" }
func (c Cursor) Region() Region { return c.region } func (c Cursor) Region() Region { return c.region }
func (c Cursor) Visible() bool { return c.visible } func (c Cursor) Visible() bool { return c.visible }
@ -499,6 +501,17 @@ func (c Cursor) String() string {
return fmt.Sprintf("Cursor[%s] region=%+v line=%d col=%d", c.id, c.region, c.Line, c.Column) return fmt.Sprintf("Cursor[%s] region=%+v line=%d col=%d", c.id, c.region, c.Line, c.Column)
} }
func (c Cursor) Draw(gtx layout.Context, r *Renderer) { func (c Cursor) Draw(gtx layout.Context, r *Renderer) {
// Clip to the cursor's allocated clip region (e.g., the editor text field)
// to prevent drawing over status bars.
var stack *clip.Stack
if c.ClipRegion.W > 0 && c.ClipRegion.H > 0 {
stack = new(clip.Stack)
*stack = clip.Rect{
Min: image.Point{X: int(r.toPx(c.ClipRegion.X)), Y: int(r.toPx(c.ClipRegion.Y))},
Max: image.Point{X: int(r.toPx(c.ClipRegion.X + c.ClipRegion.W)), Y: int(r.toPx(c.ClipRegion.Y + c.ClipRegion.H))},
}.Op().Push(gtx.Ops)
}
// Draw a thin vertical bar (e.g., width 2dp, height 18dp) at the cursor's top-left position // Draw a thin vertical bar (e.g., width 2dp, height 18dp) at the cursor's top-left position
// instead of filling the entire region, which covers the text editor. // instead of filling the entire region, which covers the text editor.
cursorRegion := Region{ cursorRegion := Region{
@ -508,6 +521,10 @@ func (c Cursor) Draw(gtx layout.Context, r *Renderer) {
H: Dp(18), H: Dp(18),
} }
r.drawBg(gtx, cursorRegion, Color{R: 0, G: 0, B: 0, A: 255}) r.drawBg(gtx, cursorRegion, Color{R: 0, G: 0, B: 0, A: 255})
if stack != nil {
stack.Pop()
}
} }
func NewCursor(id string, region Region, line, col int, blinking bool) Cursor { func NewCursor(id string, region Region, line, col int, blinking bool) Cursor {

View File

@ -280,6 +280,15 @@ func (r *Renderer) drawElement(gtx layout.Context, e Element) {
} }
} }
for _, interaction := range interactive.Interactions() { for _, interaction := range interactive.Interactions() {
if interaction.Gesture == Scroll {
reg, ok := r.scrolls[interactive.ID()]
if !ok {
reg = scrollReg{scroll: &gesture.Scroll{}}
}
reg.scroll.Add(gtx.Ops)
reg.handler = interaction.Handler
r.scrolls[interactive.ID()] = reg
}
if interaction.Gesture == Tap { if interaction.Gesture == Tap {
reg, ok := r.clicks[interactive.ID()] reg, ok := r.clicks[interactive.ID()]
if !ok { if !ok {