gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/text/editor.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package text 4 5 import ( 6 "image" 7 "image/color" 8 "math" 9 "time" 10 "unicode/utf8" 11 12 "gioui.org/ui" 13 "gioui.org/ui/gesture" 14 "gioui.org/ui/key" 15 "gioui.org/ui/layout" 16 "gioui.org/ui/paint" 17 "gioui.org/ui/pointer" 18 19 "golang.org/x/image/math/fixed" 20 ) 21 22 // Editor implements an editable and scrollable text area. 23 type Editor struct { 24 Face Face 25 Alignment Alignment 26 // SingleLine force the text to stay on a single line. 27 // SingleLine also sets the scrolling direction to 28 // horizontal. 29 SingleLine bool 30 // Submit enabled translation of carriage return keys to SubmitEvents. 31 // If not enabled, carriage returns are inserted as newlines in the text. 32 Submit bool 33 34 // Material for drawing the text. 35 Material ui.MacroOp 36 // Hint contains the text displayed to the user when the 37 // Editor is empty. 38 Hint string 39 // Mmaterial is used to draw the hint. 40 HintMaterial ui.MacroOp 41 42 oldScale int 43 blinkStart time.Time 44 focused bool 45 rr editBuffer 46 maxWidth int 47 viewSize image.Point 48 valid bool 49 lines []Line 50 dims layout.Dimensions 51 padTop, padBottom int 52 padLeft, padRight int 53 requestFocus bool 54 55 it lineIterator 56 57 // carXOff is the offset to the current caret 58 // position when moving between lines. 59 carXOff fixed.Int26_6 60 61 scroller gesture.Scroll 62 scrollOff image.Point 63 64 clicker gesture.Click 65 66 // events is the list of events not yet processed. 67 events []ui.Event 68 } 69 70 type EditorEvent interface { 71 isEditorEvent() 72 } 73 74 // A ChangeEvent is generated for every user change to the text. 75 type ChangeEvent struct{} 76 77 // A SubmitEvent is generated when Submit is set 78 // and a carriage return key is pressed. 79 type SubmitEvent struct{} 80 81 const ( 82 blinksPerSecond = 1 83 maxBlinkDuration = 10 * time.Second 84 ) 85 86 // Event returns the next available editor event, or false if none are available. 87 func (e *Editor) Event(gtx *layout.Context) (EditorEvent, bool) { 88 // Crude configuration change detection. 89 if scale := gtx.Px(ui.Sp(100)); scale != e.oldScale { 90 e.invalidate() 91 e.oldScale = scale 92 } 93 sbounds := e.scrollBounds() 94 var smin, smax int 95 var axis gesture.Axis 96 if e.SingleLine { 97 axis = gesture.Horizontal 98 smin, smax = sbounds.Min.X, sbounds.Max.X 99 } else { 100 axis = gesture.Vertical 101 smin, smax = sbounds.Min.Y, sbounds.Max.Y 102 } 103 sdist := e.scroller.Scroll(gtx.Config, gtx.Queue, axis) 104 var soff int 105 if e.SingleLine { 106 e.scrollOff.X += sdist 107 soff = e.scrollOff.X 108 } else { 109 e.scrollOff.Y += sdist 110 soff = e.scrollOff.Y 111 } 112 for _, evt := range e.clicker.Events(gtx.Queue) { 113 switch { 114 case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse, 115 evt.Type == gesture.TypeClick && evt.Source == pointer.Touch: 116 e.blinkStart = gtx.Now() 117 e.moveCoord(image.Point{ 118 X: int(math.Round(float64(evt.Position.X))), 119 Y: int(math.Round(float64(evt.Position.Y))), 120 }) 121 e.requestFocus = true 122 if e.scroller.State() != gesture.StateFlinging { 123 e.scrollToCaret(gtx) 124 } 125 } 126 } 127 if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) { 128 e.scroller.Stop() 129 } 130 e.events = append(e.events, gtx.Queue.Events(e)...) 131 return e.editorEvent(gtx) 132 } 133 134 func (e *Editor) editorEvent(gtx *layout.Context) (EditorEvent, bool) { 135 for len(e.events) > 0 { 136 ke := e.events[0] 137 copy(e.events, e.events[1:]) 138 e.events = e.events[:len(e.events)-1] 139 e.blinkStart = gtx.Now() 140 switch ke := ke.(type) { 141 case key.FocusEvent: 142 e.focused = ke.Focus 143 case key.Event: 144 if !e.focused { 145 break 146 } 147 if e.Submit && ke.Name == key.NameReturn || ke.Name == key.NameEnter { 148 if !ke.Modifiers.Contain(key.ModShift) { 149 return SubmitEvent{}, true 150 } 151 } 152 if e.command(ke) { 153 e.scrollToCaret(gtx.Config) 154 e.scroller.Stop() 155 } 156 case key.EditEvent: 157 e.scrollToCaret(gtx) 158 e.scroller.Stop() 159 e.append(ke.Text) 160 } 161 if e.rr.Changed() { 162 return ChangeEvent{}, true 163 } 164 } 165 return nil, false 166 } 167 168 func (e *Editor) caretWidth(c ui.Config) fixed.Int26_6 { 169 oneDp := c.Px(ui.Dp(1)) 170 return fixed.Int26_6(oneDp * 64) 171 } 172 173 // Focus requests the input focus for the Editor. 174 func (e *Editor) Focus() { 175 e.requestFocus = true 176 } 177 178 // Layout flushes any remaining events and lays out the editor. 179 func (e *Editor) Layout(gtx *layout.Context) { 180 cs := gtx.Constraints 181 for _, ok := e.Event(gtx); ok; _, ok = e.Event(gtx) { 182 } 183 twoDp := gtx.Px(ui.Dp(2)) 184 e.padLeft, e.padRight = twoDp, twoDp 185 maxWidth := cs.Width.Max 186 if e.SingleLine { 187 maxWidth = inf 188 } 189 if maxWidth != inf { 190 maxWidth -= e.padLeft + e.padRight 191 } 192 if maxWidth != e.maxWidth { 193 e.maxWidth = maxWidth 194 e.invalidate() 195 } 196 197 e.layout() 198 lines, size := e.lines, e.dims.Size 199 e.viewSize = cs.Constrain(size) 200 201 carLine, _, carX, carY := e.layoutCaret() 202 203 off := image.Point{ 204 X: -e.scrollOff.X + e.padLeft, 205 Y: -e.scrollOff.Y + e.padTop, 206 } 207 clip := image.Rectangle{ 208 Min: image.Point{X: 0, Y: 0}, 209 Max: image.Point{X: e.viewSize.X, Y: e.viewSize.Y}, 210 } 211 key.InputOp{Key: e, Focus: e.requestFocus}.Add(gtx.Ops) 212 e.requestFocus = false 213 e.it = lineIterator{ 214 Lines: lines, 215 Clip: clip, 216 Alignment: e.Alignment, 217 Width: e.viewWidth(), 218 Offset: off, 219 } 220 var stack ui.StackOp 221 stack.Push(gtx.Ops) 222 // Apply material. Set a default color in case the material is empty. 223 if e.rr.len() > 0 { 224 paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(gtx.Ops) 225 e.Material.Add(gtx.Ops) 226 } else { 227 paint.ColorOp{Color: color.RGBA{A: 0xaa}}.Add(gtx.Ops) 228 e.HintMaterial.Add(gtx.Ops) 229 } 230 for { 231 str, lineOff, ok := e.it.Next() 232 if !ok { 233 break 234 } 235 var stack ui.StackOp 236 stack.Push(gtx.Ops) 237 ui.TransformOp{}.Offset(lineOff).Add(gtx.Ops) 238 e.Face.Path(str).Add(gtx.Ops) 239 paint.PaintOp{Rect: toRectF(clip).Sub(lineOff)}.Add(gtx.Ops) 240 stack.Pop() 241 } 242 if e.focused { 243 now := gtx.Now() 244 dt := now.Sub(e.blinkStart) 245 blinking := dt < maxBlinkDuration 246 const timePerBlink = time.Second / blinksPerSecond 247 nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2)) 248 on := !blinking || dt%timePerBlink < timePerBlink/2 249 if on { 250 carWidth := e.caretWidth(gtx) 251 carX -= carWidth / 2 252 carAsc, carDesc := -lines[carLine].Bounds.Min.Y, lines[carLine].Bounds.Max.Y 253 carRect := image.Rectangle{ 254 Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()}, 255 Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), Y: carY + carDesc.Ceil()}, 256 } 257 carRect = carRect.Add(image.Point{ 258 X: -e.scrollOff.X + e.padLeft, 259 Y: -e.scrollOff.Y + e.padTop, 260 }) 261 carRect = clip.Intersect(carRect) 262 if !carRect.Empty() { 263 paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(gtx.Ops) 264 e.Material.Add(gtx.Ops) 265 paint.PaintOp{Rect: toRectF(carRect)}.Add(gtx.Ops) 266 } 267 } 268 if blinking { 269 redraw := ui.InvalidateOp{At: nextBlink} 270 redraw.Add(gtx.Ops) 271 } 272 } 273 stack.Pop() 274 275 baseline := e.padTop + e.dims.Baseline 276 pointerPadding := gtx.Px(ui.Dp(4)) 277 r := image.Rectangle{Max: e.viewSize} 278 r.Min.X -= pointerPadding 279 r.Min.Y -= pointerPadding 280 r.Max.X += pointerPadding 281 r.Max.X += pointerPadding 282 pointer.RectAreaOp{Rect: r}.Add(gtx.Ops) 283 e.scroller.Add(gtx.Ops) 284 e.clicker.Add(gtx.Ops) 285 gtx.Dimensions = layout.Dimensions{Size: e.viewSize, Baseline: baseline} 286 } 287 288 // Text returns the contents of the editor. 289 func (e *Editor) Text() string { 290 return e.rr.String() 291 } 292 293 // SetText replaces the contents of the editor. 294 func (e *Editor) SetText(s string) { 295 e.rr = editBuffer{} 296 e.carXOff = 0 297 e.prepend(s) 298 } 299 300 func (e *Editor) layout() { 301 e.adjustScroll() 302 if e.valid { 303 return 304 } 305 e.layoutText() 306 e.valid = true 307 } 308 309 func (e *Editor) scrollBounds() image.Rectangle { 310 var b image.Rectangle 311 if e.SingleLine { 312 if len(e.lines) > 0 { 313 b.Min.X = align(e.Alignment, e.lines[0].Width, e.viewWidth()).Floor() 314 if b.Min.X > 0 { 315 b.Min.X = 0 316 } 317 } 318 b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X 319 } else { 320 b.Max.Y = e.dims.Size.Y - e.viewSize.Y 321 } 322 return b 323 } 324 325 func (e *Editor) adjustScroll() { 326 b := e.scrollBounds() 327 if e.scrollOff.X > b.Max.X { 328 e.scrollOff.X = b.Max.X 329 } 330 if e.scrollOff.X < b.Min.X { 331 e.scrollOff.X = b.Min.X 332 } 333 if e.scrollOff.Y > b.Max.Y { 334 e.scrollOff.Y = b.Max.Y 335 } 336 if e.scrollOff.Y < b.Min.Y { 337 e.scrollOff.Y = b.Min.Y 338 } 339 } 340 341 func (e *Editor) moveCoord(pos image.Point) { 342 e.layout() 343 var ( 344 prevDesc fixed.Int26_6 345 carLine int 346 y int 347 ) 348 for _, l := range e.lines { 349 y += (prevDesc + l.Ascent).Ceil() 350 prevDesc = l.Descent 351 if y+prevDesc.Ceil() >= pos.Y+e.scrollOff.Y-e.padTop { 352 break 353 } 354 carLine++ 355 } 356 x := fixed.I(pos.X + e.scrollOff.X - e.padLeft) 357 e.moveToLine(x, carLine) 358 } 359 360 func (e *Editor) layoutText() { 361 s := e.rr.String() 362 if s == "" { 363 s = e.Hint 364 } 365 textLayout := e.Face.Layout(s, LayoutOptions{SingleLine: e.SingleLine, MaxWidth: e.maxWidth}) 366 lines := textLayout.Lines 367 dims := linesDimens(lines) 368 for i := 0; i < len(lines)-1; i++ { 369 s := lines[i].Text.String 370 // To avoid layout flickering while editing, assume a soft newline takes 371 // up all available space. 372 if len(s) > 0 { 373 r, _ := utf8.DecodeLastRuneInString(s) 374 if r != '\n' { 375 dims.Size.X = e.maxWidth 376 break 377 } 378 } 379 } 380 padTop, padBottom := textPadding(lines) 381 dims.Size.Y += padTop + padBottom 382 dims.Size.X += e.padLeft + e.padRight 383 e.padTop = padTop 384 e.padBottom = padBottom 385 e.lines, e.dims = lines, dims 386 } 387 388 func (e *Editor) viewWidth() int { 389 return e.viewSize.X - e.padLeft - e.padRight 390 } 391 392 func (e *Editor) layoutCaret() (carLine, carCol int, x fixed.Int26_6, y int) { 393 e.layout() 394 var idx int 395 var prevDesc fixed.Int26_6 396 loop: 397 for carLine = 0; carLine < len(e.lines); carLine++ { 398 l := e.lines[carLine] 399 y += (prevDesc + l.Ascent).Ceil() 400 prevDesc = l.Descent 401 if carLine == len(e.lines)-1 || idx+len(l.Text.String) > e.rr.caret { 402 str := l.Text.String 403 for _, adv := range l.Text.Advances { 404 if idx == e.rr.caret { 405 break loop 406 } 407 x += adv 408 _, s := utf8.DecodeRuneInString(str) 409 idx += s 410 str = str[s:] 411 carCol++ 412 } 413 break 414 } 415 idx += len(l.Text.String) 416 } 417 x += align(e.Alignment, e.lines[carLine].Width, e.viewWidth()) 418 return 419 } 420 421 func (e *Editor) invalidate() { 422 e.valid = false 423 } 424 425 func (e *Editor) deleteRune() { 426 e.rr.deleteRune() 427 e.carXOff = 0 428 e.invalidate() 429 } 430 431 func (e *Editor) deleteRuneForward() { 432 e.rr.deleteRuneForward() 433 e.carXOff = 0 434 e.invalidate() 435 } 436 437 func (e *Editor) append(s string) { 438 if e.SingleLine && s == "\n" { 439 return 440 } 441 e.prepend(s) 442 e.rr.caret += len(s) 443 } 444 445 func (e *Editor) prepend(s string) { 446 e.rr.prepend(s) 447 e.carXOff = 0 448 e.invalidate() 449 } 450 451 func (e *Editor) movePages(pages int) { 452 e.layout() 453 _, _, carX, carY := e.layoutCaret() 454 y := carY + pages*e.viewSize.Y 455 var ( 456 prevDesc fixed.Int26_6 457 carLine2 int 458 ) 459 y2 := e.lines[0].Ascent.Ceil() 460 for i := 1; i < len(e.lines); i++ { 461 if y2 >= y { 462 break 463 } 464 l := e.lines[i] 465 h := (prevDesc + l.Ascent).Ceil() 466 prevDesc = l.Descent 467 if y2+h-y >= y-y2 { 468 break 469 } 470 y2 += h 471 carLine2++ 472 } 473 e.carXOff = e.moveToLine(carX+e.carXOff, carLine2) 474 } 475 476 func (e *Editor) moveToLine(carX fixed.Int26_6, carLine2 int) fixed.Int26_6 { 477 e.layout() 478 carLine, carCol, _, _ := e.layoutCaret() 479 if carLine2 < 0 { 480 carLine2 = 0 481 } 482 if carLine2 >= len(e.lines) { 483 carLine2 = len(e.lines) - 1 484 } 485 // Move to start of line. 486 for i := carCol - 1; i >= 0; i-- { 487 _, s := e.rr.runeBefore(e.rr.caret) 488 e.rr.caret -= s 489 } 490 if carLine2 != carLine { 491 // Move to start of line2. 492 if carLine2 > carLine { 493 for i := carLine; i < carLine2; i++ { 494 e.rr.caret += len(e.lines[i].Text.String) 495 } 496 } else { 497 for i := carLine - 1; i >= carLine2; i-- { 498 e.rr.caret -= len(e.lines[i].Text.String) 499 } 500 } 501 } 502 l2 := e.lines[carLine2] 503 carX2 := align(e.Alignment, l2.Width, e.viewWidth()) 504 // Only move past the end of the last line 505 end := 0 506 if carLine2 < len(e.lines)-1 { 507 end = 1 508 } 509 // Move to rune closest to previous horizontal position. 510 for i := 0; i < len(l2.Text.Advances)-end; i++ { 511 adv := l2.Text.Advances[i] 512 if carX2 >= carX { 513 break 514 } 515 if carX2+adv-carX >= carX-carX2 { 516 break 517 } 518 carX2 += adv 519 _, s := e.rr.runeAt(e.rr.caret) 520 e.rr.caret += s 521 } 522 return carX - carX2 523 } 524 525 func (e *Editor) moveLeft() { 526 e.rr.moveLeft() 527 e.carXOff = 0 528 } 529 530 func (e *Editor) moveRight() { 531 e.rr.moveRight() 532 e.carXOff = 0 533 } 534 535 func (e *Editor) moveStart() { 536 carLine, carCol, x, _ := e.layoutCaret() 537 advances := e.lines[carLine].Text.Advances 538 for i := carCol - 1; i >= 0; i-- { 539 _, s := e.rr.runeBefore(e.rr.caret) 540 e.rr.caret -= s 541 x -= advances[i] 542 } 543 e.carXOff = -x 544 } 545 546 func (e *Editor) moveEnd() { 547 carLine, carCol, x, _ := e.layoutCaret() 548 l := e.lines[carLine] 549 // Only move past the end of the last line 550 end := 0 551 if carLine < len(e.lines)-1 { 552 end = 1 553 } 554 for i := carCol; i < len(l.Text.Advances)-end; i++ { 555 adv := l.Text.Advances[i] 556 _, s := e.rr.runeAt(e.rr.caret) 557 e.rr.caret += s 558 x += adv 559 } 560 a := align(e.Alignment, l.Width, e.viewWidth()) 561 e.carXOff = l.Width + a - x 562 } 563 564 func (e *Editor) scrollToCaret(c ui.Config) { 565 carWidth := e.caretWidth(c) 566 carLine, _, x, y := e.layoutCaret() 567 l := e.lines[carLine] 568 if e.SingleLine { 569 minx := (x - carWidth/2).Ceil() 570 if d := minx - e.scrollOff.X + e.padLeft; d < 0 { 571 e.scrollOff.X += d 572 } 573 maxx := (x + carWidth/2).Ceil() 574 if d := maxx - (e.scrollOff.X + e.viewSize.X - e.padRight); d > 0 { 575 e.scrollOff.X += d 576 } 577 } else { 578 miny := y + l.Bounds.Min.Y.Floor() 579 if d := miny - e.scrollOff.Y + e.padTop; d < 0 { 580 e.scrollOff.Y += d 581 } 582 maxy := y + l.Bounds.Max.Y.Ceil() 583 if d := maxy - (e.scrollOff.Y + e.viewSize.Y - e.padBottom); d > 0 { 584 e.scrollOff.Y += d 585 } 586 } 587 } 588 589 func (e *Editor) command(k key.Event) bool { 590 switch k.Name { 591 case key.NameReturn, key.NameEnter: 592 e.append("\n") 593 case key.NameDeleteBackward: 594 e.deleteRune() 595 case key.NameDeleteForward: 596 e.deleteRuneForward() 597 case key.NameUpArrow: 598 line, _, carX, _ := e.layoutCaret() 599 e.carXOff = e.moveToLine(carX+e.carXOff, line-1) 600 case key.NameDownArrow: 601 line, _, carX, _ := e.layoutCaret() 602 e.carXOff = e.moveToLine(carX+e.carXOff, line+1) 603 case key.NameLeftArrow: 604 e.moveLeft() 605 case key.NameRightArrow: 606 e.moveRight() 607 case key.NamePageUp: 608 e.movePages(-1) 609 case key.NamePageDown: 610 e.movePages(+1) 611 case key.NameHome: 612 e.moveStart() 613 case key.NameEnd: 614 e.moveEnd() 615 default: 616 return false 617 } 618 return true 619 } 620 621 func (s ChangeEvent) isEditorEvent() {} 622 func (s SubmitEvent) isEditorEvent() {}