github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/framework/gio.go (about) 1 package framework 2 3 import ( 4 "bytes" 5 "fmt" 6 "image" 7 "image/color" 8 "io" 9 "math" 10 "os" 11 "sync" 12 "sync/atomic" 13 "time" 14 "unicode/utf8" 15 "unsafe" 16 17 "github.com/gop9/olt/gio/app" 18 "github.com/gop9/olt/gio/f32" 19 "github.com/gop9/olt/gio/font/opentype" 20 gkey "github.com/gop9/olt/gio/io/key" 21 "github.com/gop9/olt/gio/io/pointer" 22 "github.com/gop9/olt/gio/io/profile" 23 "github.com/gop9/olt/gio/io/system" 24 "github.com/gop9/olt/gio/op" 25 gioclip "github.com/gop9/olt/gio/op/clip" 26 "github.com/gop9/olt/gio/op/paint" 27 "github.com/gop9/olt/gio/text" 28 "github.com/gop9/olt/gio/unit" 29 30 "github.com/gop9/olt/framework/clipboard" 31 "github.com/gop9/olt/framework/command" 32 "github.com/gop9/olt/framework/font" 33 "github.com/gop9/olt/framework/label" 34 "github.com/gop9/olt/framework/rect" 35 36 ifont "golang.org/x/image/font" 37 "golang.org/x/image/math/fixed" 38 ) 39 40 type masterWindow struct { 41 masterWindowCommon 42 43 Title string 44 initialSize image.Point 45 size image.Point 46 47 w *app.Window 48 ops op.Ops 49 50 textbuffer bytes.Buffer 51 52 closed bool 53 } 54 55 var clipboardStarted bool = false 56 var clipboardMu sync.Mutex 57 58 func NewMasterWindowSize(flags WindowFlags, title string, sz image.Point, updatefn UpdateFn) MasterWindow { 59 ctx := &context{} 60 wnd := &masterWindow{} 61 62 wnd.masterWindowCommonInit(ctx, flags, updatefn, wnd) 63 64 wnd.Title = title 65 wnd.initialSize = sz 66 67 clipboardMu.Lock() 68 if !clipboardStarted { 69 clipboardStarted = true 70 clipboard.Start() 71 } 72 clipboardMu.Unlock() 73 74 return wnd 75 } 76 77 func (mw *masterWindow) Main() { 78 go func() { 79 mw.w = app.NewWindow(app.Title(mw.Title), app.Size(unit.Px(float32(mw.ctx.scale(mw.initialSize.X))), unit.Px(float32(mw.ctx.scale(mw.initialSize.Y))))) 80 mw.main() 81 }() 82 go mw.updater() 83 app.Main() 84 } 85 86 func (mw *masterWindow) Lock() { 87 mw.uilock.Lock() 88 } 89 90 func (mw *masterWindow) Unlock() { 91 mw.uilock.Unlock() 92 } 93 94 func (mw *masterWindow) Close() { 95 os.Exit(0) // Bad... 96 } 97 98 func (mw *masterWindow) Closed() bool { 99 mw.uilock.Lock() 100 defer mw.uilock.Unlock() 101 return mw.closed 102 } 103 104 func (mw *masterWindow) main() { 105 for { 106 e := <-mw.w.Events() 107 switch e := e.(type) { 108 case system.DestroyEvent: 109 mw.uilock.Lock() 110 mw.closed = true 111 mw.uilock.Unlock() 112 if e.Err != nil { 113 fmt.Fprintf(os.Stderr, "error: %v\n", e.Err) 114 } 115 return 116 117 case system.FrameEvent: 118 mw.size = e.Size 119 mw.uilock.Lock() 120 mw.prevCmds = mw.prevCmds[:0] 121 mw.updateLocked() 122 mw.uilock.Unlock() 123 124 e.Frame(&mw.ops) 125 } 126 } 127 } 128 129 func (mw *masterWindow) processPointerEvent(e pointer.Event) { 130 switch e.Type { 131 case pointer.Release, pointer.Cancel: 132 for i := range mw.ctx.Input.Mouse.Buttons { 133 btn := &mw.ctx.Input.Mouse.Buttons[i] 134 if btn.Down { 135 btn.Down = false 136 btn.Clicked = true 137 } 138 } 139 140 case pointer.Press: 141 var button pointer.Buttons 142 143 switch { 144 case e.Buttons.Contain(pointer.ButtonLeft): 145 button = pointer.ButtonLeft 146 case e.Buttons.Contain(pointer.ButtonRight): 147 button = pointer.ButtonRight 148 case e.Buttons.Contain(pointer.ButtonMiddle): 149 button = pointer.ButtonMiddle 150 } 151 152 if button == pointer.ButtonRight && e.Modifiers.Contain(gkey.ModCtrl) { 153 button = pointer.ButtonLeft 154 } 155 156 down := e.Type == pointer.Press 157 btn := &mw.ctx.Input.Mouse.Buttons[button] 158 if btn.Down == down { 159 break 160 } 161 162 if down { 163 btn.ClickedPos.X = int(e.Position.X) 164 btn.ClickedPos.Y = int(e.Position.Y) 165 } 166 btn.Clicked = true 167 btn.Down = down 168 169 case pointer.Move: 170 mw.ctx.Input.Mouse.Pos.X = int(e.Position.X) 171 mw.ctx.Input.Mouse.Pos.Y = int(e.Position.Y) 172 mw.ctx.Input.Mouse.Delta = mw.ctx.Input.Mouse.Pos.Sub(mw.ctx.Input.Mouse.Prev) 173 174 if e.Scroll.Y < 0 { 175 mw.ctx.Input.Mouse.ScrollDelta++ 176 } else if e.Scroll.Y > 0 { 177 mw.ctx.Input.Mouse.ScrollDelta-- 178 } 179 } 180 } 181 182 var runeToCode = map[string]gkey.Code{} 183 184 func init() { 185 for i := byte('a'); i <= 'z'; i++ { 186 c := gkey.Code((i - 'a') + 4) 187 runeToCode[string([]byte{i})] = c 188 runeToCode[string([]byte{i - 0x20})] = c 189 } 190 // 191 //runeToCode["\t"] = gkey.CodeTab 192 //runeToCode[" "] = gkey.CodeSpacebar 193 //runeToCode["-"] = gkey.CodeHyphenMinus 194 //runeToCode["="] = gkey.CodeEqualSign 195 //runeToCode["["] = gkey.CodeLeftSquareBracket 196 //runeToCode["]"] = gkey.CodeRightSquareBracket 197 //runeToCode["\\"] = gkey.CodeBackslash 198 //runeToCode[";"] = gkey.CodeSemicolon 199 //runeToCode["\""] = gkey.CodeApostrophe 200 //runeToCode["`"] = gkey.CodeGraveAccent 201 //runeToCode[","] = gkey.CodeComma 202 //runeToCode["."] = gkey.CodeFullStop 203 //runeToCode["/"] = gkey.CodeSlash 204 // 205 //runeToCode[gkey.NameLeftArrow] = gkey.CodeLeftArrow 206 //runeToCode[gkey.NameRightArrow] = gkey.CodeRightArrow 207 //runeToCode[gkey.NameUpArrow] = gkey.CodeUpArrow 208 //runeToCode[gkey.NameDownArrow] = gkey.CodeDownArrow 209 //runeToCode[gkey.NameReturn] = gkey.CodeReturnEnter 210 //runeToCode[gkey.NameEnter] = gkey.CodeReturnEnter 211 //runeToCode[gkey.NameEscape] = gkey.CodeEscape 212 //runeToCode[gkey.NameHome] = gkey.CodeHome 213 //runeToCode[gkey.NameEnd] = gkey.CodeEnd 214 //runeToCode[gkey.NameDeleteBackward] = gkey.CodeDeleteBackspace 215 //runeToCode[gkey.NameDeleteForward] = gkey.CodeDeleteForward 216 //runeToCode[gkey.NamePageUp] = gkey.CodePageUp 217 //runeToCode[gkey.NamePageDown] = gkey.CodePageDown 218 } 219 220 func gio2mobileKey(e gkey.Event) gkey.Event { 221 var mod gkey.Modifiers 222 223 //if e.Modifiers.Contain(gkey.ModCommand) { 224 // mod |= gkey.ModMeta 225 //} 226 //if e.Modifiers.Contain(gkey.ModCtrl) { 227 // mod |= gkey.ModControl 228 //} 229 //if e.Modifiers.Contain(gkey.ModAlt) { 230 // mod |= gkey.ModAlt 231 //} 232 //if e.Modifiers.Contain(gkey.ModSuper) { 233 // mod |= gkey.ModMeta 234 //} 235 236 var name rune 237 238 for _, ch := range e.Name { 239 name = ch 240 break 241 } 242 243 return gkey.Event{ 244 Rune: name, 245 Code: runeToCode[e.Name], 246 Modifiers: mod, 247 Direction: gkey.DirRelease, 248 } 249 } 250 251 func (w *masterWindow) updater() { 252 var down bool 253 for { 254 if down { 255 time.Sleep(10 * time.Millisecond) 256 } else { 257 time.Sleep(20 * time.Millisecond) 258 } 259 func() { 260 w.uilock.Lock() 261 defer w.uilock.Unlock() 262 if w.closed { 263 return 264 } 265 changed := atomic.LoadInt32(&w.ctx.changed) 266 if changed > 0 { 267 atomic.AddInt32(&w.ctx.changed, -1) 268 w.w.Invalidate() 269 } 270 }() 271 } 272 } 273 274 func (mw *masterWindow) updateLocked() { 275 perfString := "" 276 q := mw.w.Queue() 277 for _, e := range q.Events(mw.ctx) { 278 switch e := e.(type) { 279 case profile.Event: 280 perfString = e.Timings 281 case pointer.Event: 282 mw.processPointerEvent(e) 283 case gkey.EditEvent: 284 io.WriteString(&mw.textbuffer, e.Text) 285 286 case gkey.Event: 287 switch e.Name { 288 case gkey.NameEnter, gkey.NameReturn: 289 io.WriteString(&mw.textbuffer, "\n") 290 } 291 mw.ctx.Input.Keyboard.Keys = append(mw.ctx.Input.Keyboard.Keys, gio2mobileKey(e)) 292 } 293 } 294 295 mw.ctx.Windows[0].Bounds = rect.Rect{X: 0, Y: 0, W: mw.size.X, H: mw.size.Y} 296 in := &mw.ctx.Input 297 in.Mouse.clip = nk_null_rect 298 in.Keyboard.Text = mw.textbuffer.String() 299 mw.textbuffer.Reset() 300 301 var t0, t1, te time.Time 302 if perfUpdate || mw.Perf { 303 t0 = time.Now() 304 } 305 306 if dumpFrame && !perfUpdate { 307 panic("dumpFrame") 308 } 309 310 mw.ctx.Update() 311 312 if perfUpdate || mw.Perf { 313 t1 = time.Now() 314 } 315 nprimitives := mw.draw() 316 if perfUpdate && nprimitives > 0 { 317 te = time.Now() 318 319 fps := 1.0 / te.Sub(t0).Seconds() 320 321 fmt.Printf("Update %0.4f msec = %0.4f updatefn + %0.4f draw (%d primitives) [max fps %0.2f]\n", te.Sub(t0).Seconds()*1000, t1.Sub(t0).Seconds()*1000, te.Sub(t1).Seconds()*1000, nprimitives, fps) 322 } 323 if mw.Perf && nprimitives > 0 { 324 te = time.Now() 325 fps := 1.0 / te.Sub(t0).Seconds() 326 327 s := fmt.Sprintf("%0.4fms + %0.4fms (%0.2f)\n%s", t1.Sub(t0).Seconds()*1000, te.Sub(t1).Seconds()*1000, fps, perfString) 328 329 font := mw.Style().Font 330 txt := fontFace2fontFace(&font).layout(s, -1) 331 332 bounds := image.Point{X: maxLinesWidth(txt.Lines), Y: (txt.Lines[0].Ascent + txt.Lines[0].Descent).Ceil() * 2} 333 334 pos := mw.size 335 pos.Y -= bounds.Y 336 pos.X -= bounds.X 337 338 paintRect := f32.Rectangle{f32.Point{float32(pos.X), float32(pos.Y)}, f32.Point{float32(pos.X + bounds.X), float32(pos.Y + bounds.Y)}} 339 340 var stack op.StackOp 341 stack.Push(&mw.ops) 342 paint.ColorOp{Color: color.RGBA{0xff, 0xff, 0xff, 0xff}}.Add(&mw.ops) 343 paint.PaintOp{Rect: paintRect}.Add(&mw.ops) 344 stack.Pop() 345 346 drawText(&mw.ops, txt, font, color.RGBA{0x00, 0x00, 0x00, 0xff}, pos, bounds, paintRect) 347 } 348 } 349 350 func (w *masterWindow) draw() int { 351 if !w.drawChanged() { 352 return 0 353 } 354 355 w.prevCmds = append(w.prevCmds[:0], w.ctx.cmds...) 356 357 return w.ctx.Draw(&w.ops, w.size, w.Perf) 358 } 359 360 func (ctx *context) Draw(ops *op.Ops, size image.Point, perf bool) int { 361 ops.Reset() 362 363 if perf { 364 profile.Op{ctx}.Add(ops) 365 } 366 pointer.InputOp{ctx, false}.Add(ops) 367 gkey.InputOp{ctx, true}.Add(ops) 368 369 var scissorStack op.StackOp 370 scissorless := true 371 372 for i := range ctx.cmds { 373 icmd := &ctx.cmds[i] 374 switch icmd.Kind { 375 case command.ScissorCmd: 376 if !scissorless { 377 scissorStack.Pop() 378 } 379 scissorStack.Push(ops) 380 gioclip.Rect{Rect: n2fRect(icmd.Rect)}.Op(ops).Add(ops) 381 scissorless = false 382 383 case command.LineCmd: 384 cmd := icmd.Line 385 386 var stack op.StackOp 387 stack.Push(ops) 388 paint.ColorOp{Color: cmd.Color}.Add(ops) 389 390 h1 := int(cmd.LineThickness / 2) 391 h2 := int(cmd.LineThickness) - h1 392 393 if cmd.Begin.X == cmd.End.X { 394 y0, y1 := cmd.Begin.Y, cmd.End.Y 395 if y0 > y1 { 396 y0, y1 = y1, y0 397 } 398 paint.PaintOp{Rect: f32.Rectangle{ 399 f32.Point{float32(cmd.Begin.X - h1), float32(y0)}, 400 f32.Point{float32(cmd.Begin.X + h2), float32(y1)}}}.Add(ops) 401 } else if cmd.Begin.Y == cmd.End.Y { 402 x0, x1 := cmd.Begin.X, cmd.End.X 403 if x0 > x1 { 404 x0, x1 = x1, x0 405 } 406 paint.PaintOp{Rect: f32.Rectangle{ 407 f32.Point{float32(x0), float32(cmd.Begin.Y - h1)}, 408 f32.Point{float32(x1), float32(cmd.Begin.Y + h2)}}}.Add(ops) 409 } else { 410 m := float32(cmd.Begin.Y-cmd.End.Y) / float32(cmd.Begin.X-cmd.End.X) 411 invm := -1 / m 412 413 xadv := float32(math.Sqrt(float64(cmd.LineThickness*cmd.LineThickness) / (4 * float64((invm*invm + 1))))) 414 yadv := xadv * invm 415 416 var p gioclip.Path 417 p.Begin(ops) 418 419 pa := f32.Point{float32(cmd.Begin.X) - xadv, float32(cmd.Begin.Y) - yadv} 420 p.Move(pa) 421 pb := f32.Point{2 * xadv, 2 * yadv} 422 p.Line(pb) 423 pc := f32.Point{float32(cmd.End.X - cmd.Begin.X), float32(cmd.End.Y - cmd.Begin.Y)} 424 p.Line(pc) 425 pd := f32.Point{-2 * xadv, -2 * yadv} 426 p.Line(pd) 427 p.Line(f32.Point{float32(cmd.Begin.X - cmd.End.X), float32(cmd.Begin.Y - cmd.End.Y)}) 428 429 p.End().Add(ops) 430 431 pb = pb.Add(pa) 432 pc = pc.Add(pb) 433 pd = pd.Add(pc) 434 435 minp := f32.Point{ 436 min4(pa.X, pb.X, pc.X, pd.X), 437 min4(pa.Y, pb.Y, pc.Y, pd.Y)} 438 maxp := f32.Point{ 439 max4(pa.X, pb.X, pc.X, pd.X), 440 max4(pa.Y, pb.Y, pc.Y, pd.Y)} 441 442 paint.PaintOp{Rect: f32.Rectangle{minp, maxp}}.Add(ops) 443 } 444 445 stack.Pop() 446 447 case command.RectFilledCmd: 448 cmd := icmd.RectFilled 449 // rounding is true if rounding has been requested AND we can draw it 450 rounding := cmd.Rounding > 0 && int(cmd.Rounding*2) < icmd.W && int(cmd.Rounding*2) < icmd.H 451 452 var stack op.StackOp 453 stack.Push(ops) 454 paint.ColorOp{Color: cmd.Color}.Add(ops) 455 456 if rounding { 457 const c = 0.55228475 // 4*(sqrt(2)-1)/3 458 459 x, y, w, h := float32(icmd.X), float32(icmd.Y), float32(icmd.W), float32(icmd.H) 460 r := float32(cmd.Rounding) 461 462 var b gioclip.Path 463 b.Begin(ops) 464 b.Move(f32.Point{X: x + w, Y: y + h - r}) 465 b.Cube(f32.Point{X: 0, Y: r * c}, f32.Point{X: -r + r*c, Y: r}, f32.Point{X: -r, Y: r}) // SE 466 b.Line(f32.Point{X: r - w + r, Y: 0}) 467 b.Cube(f32.Point{X: -r * c, Y: 0}, f32.Point{X: -r, Y: -r + r*c}, f32.Point{X: -r, Y: -r}) // SW 468 b.Line(f32.Point{X: 0, Y: r - h + r}) 469 b.Cube(f32.Point{X: 0, Y: -r * c}, f32.Point{X: r - r*c, Y: -r}, f32.Point{X: r, Y: -r}) // NW 470 b.Line(f32.Point{X: w - r - r, Y: 0}) 471 b.Cube(f32.Point{X: r * c, Y: 0}, f32.Point{X: r, Y: r - r*c}, f32.Point{X: r, Y: r}) // NE 472 b.End().Add(ops) 473 } 474 475 paint.PaintOp{Rect: n2fRect(icmd.Rect)}.Add(ops) 476 stack.Pop() 477 478 case command.TriangleFilledCmd: 479 cmd := icmd.TriangleFilled 480 481 var stack op.StackOp 482 stack.Push(ops) 483 484 paint.ColorOp{cmd.Color}.Add(ops) 485 486 var p gioclip.Path 487 p.Begin(ops) 488 p.Move(f32.Point{float32(cmd.A.X), float32(cmd.A.Y)}) 489 p.Line(f32.Point{float32(cmd.B.X - cmd.A.X), float32(cmd.B.Y - cmd.A.Y)}) 490 p.Line(f32.Point{float32(cmd.C.X - cmd.B.X), float32(cmd.C.Y - cmd.B.Y)}) 491 p.Line(f32.Point{float32(cmd.A.X - cmd.C.X), float32(cmd.A.Y - cmd.C.Y)}) 492 p.End().Add(ops) 493 494 pmin := f32.Point{ 495 min2(min2(float32(cmd.A.X), float32(cmd.B.X)), float32(cmd.C.X)), 496 min2(min2(float32(cmd.A.Y), float32(cmd.B.Y)), float32(cmd.C.Y))} 497 498 pmax := f32.Point{ 499 max2(max2(float32(cmd.A.X), float32(cmd.B.X)), float32(cmd.C.X)), 500 max2(max2(float32(cmd.A.Y), float32(cmd.B.Y)), float32(cmd.C.Y))} 501 502 paint.PaintOp{Rect: f32.Rectangle{pmin, pmax}}.Add(ops) 503 504 stack.Pop() 505 506 case command.CircleFilledCmd: 507 var stack op.StackOp 508 stack.Push(ops) 509 510 paint.ColorOp{icmd.CircleFilled.Color}.Add(ops) 511 512 r := min2(float32(icmd.W), float32(icmd.H)) / 2 513 514 const c = 0.55228475 // 4*(sqrt(2)-1)/3 515 var b gioclip.Path 516 b.Begin(ops) 517 b.Move(f32.Point{X: float32(icmd.X) + r*2, Y: float32(icmd.Y) + r}) 518 b.Cube(f32.Point{X: 0, Y: r * c}, f32.Point{X: -r + r*c, Y: r}, f32.Point{X: -r, Y: r}) // SE 519 b.Cube(f32.Point{X: -r * c, Y: 0}, f32.Point{X: -r, Y: -r + r*c}, f32.Point{X: -r, Y: -r}) // SW 520 b.Cube(f32.Point{X: 0, Y: -r * c}, f32.Point{X: r - r*c, Y: -r}, f32.Point{X: r, Y: -r}) // NW 521 b.Cube(f32.Point{X: r * c, Y: 0}, f32.Point{X: r, Y: r - r*c}, f32.Point{X: r, Y: r}) // NE 522 b.End().Add(ops) 523 524 paint.PaintOp{Rect: n2fRect(icmd.Rect)}.Add(ops) 525 526 stack.Pop() 527 528 case command.ImageCmd: 529 var stack op.StackOp 530 stack.Push(ops) 531 //TODO: this should be retained between frames somehow... 532 paint.NewImageOp(icmd.Image.Img).Add(ops) 533 paint.PaintOp{n2fRect(icmd.Rect)}.Add(ops) 534 stack.Pop() 535 536 case command.TextCmd: 537 txt := fontFace2fontFace(&icmd.Text.Face).layout(icmd.Text.String, -1) 538 if len(txt.Lines) <= 0 { 539 continue 540 } 541 542 bounds := image.Point{X: maxLinesWidth(txt.Lines), Y: (txt.Lines[0].Ascent + txt.Lines[0].Descent).Ceil()} 543 if bounds.X > icmd.W { 544 bounds.X = icmd.W 545 } 546 if bounds.Y > icmd.H { 547 bounds.Y = icmd.H 548 } 549 550 drawText(ops, txt, icmd.Text.Face, icmd.Text.Foreground, image.Point{icmd.X, icmd.Y}, bounds, n2fRect(icmd.Rect)) 551 552 default: 553 panic(UnknownCommandErr) 554 } 555 } 556 557 return len(ctx.cmds) 558 } 559 560 func n2fRect(r rect.Rect) f32.Rectangle { 561 return f32.Rectangle{ 562 Min: f32.Point{float32(r.X), float32(r.Y)}, 563 Max: f32.Point{float32(r.X + r.W), float32(r.Y + r.H)}} 564 } 565 566 func i2fRect(r image.Rectangle) f32.Rectangle { 567 return f32.Rectangle{ 568 Min: f32.Point{X: float32(r.Min.X), Y: float32(r.Min.Y)}, 569 Max: f32.Point{X: float32(r.Max.X), Y: float32(r.Max.Y)}} 570 } 571 572 func min4(a, b, c, d float32) float32 { 573 return min2(min2(a, b), min2(c, d)) 574 } 575 576 func min2(a, b float32) float32 { 577 if a < b { 578 return a 579 } 580 return b 581 } 582 583 func max4(a, b, c, d float32) float32 { 584 return max2(max2(a, b), max2(c, d)) 585 } 586 587 func max2(a, b float32) float32 { 588 if a > b { 589 return a 590 } 591 return b 592 } 593 594 func textPadding(lines []text.Line) (padding image.Rectangle) { 595 if len(lines) == 0 { 596 return 597 } 598 first := lines[0] 599 if d := first.Ascent + first.Bounds.Min.Y; d < 0 { 600 padding.Min.Y = d.Ceil() 601 } 602 last := lines[len(lines)-1] 603 if d := last.Bounds.Max.Y - last.Descent; d > 0 { 604 padding.Max.Y = d.Ceil() 605 } 606 if d := first.Bounds.Min.X; d < 0 { 607 padding.Min.X = d.Ceil() 608 } 609 if d := first.Bounds.Max.X - first.Width; d > 0 { 610 padding.Max.X = d.Ceil() 611 } 612 return 613 } 614 615 func clipLine(line text.Line, clipObject image.Rectangle) (text.String, f32.Point, bool) { 616 off := fixed.Point26_6{X: fixed.I(0), Y: fixed.I(line.Ascent.Ceil())} 617 str := line.Text 618 for len(str.Advances) > 0 { 619 adv := str.Advances[0] 620 if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= clipObject.Min.X { 621 break 622 } 623 off.X += adv 624 _, s := utf8.DecodeRuneInString(str.String) 625 str.String = str.String[s:] 626 str.Advances = str.Advances[1:] 627 } 628 n := 0 629 endx := off.X 630 for i, adv := range str.Advances { 631 if (endx + line.Bounds.Min.X).Floor() > clipObject.Max.X { 632 str.String = str.String[:n] 633 str.Advances = str.Advances[:i] 634 break 635 } 636 _, s := utf8.DecodeRuneInString(str.String[n:]) 637 n += s 638 endx += adv 639 } 640 offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64} 641 return str, offf, true 642 } 643 644 func maxLinesWidth(lines []text.Line) int { 645 w := 0 646 for _, line := range lines { 647 if line.Width.Ceil() > w { 648 w = line.Width.Ceil() 649 } 650 } 651 return w 652 } 653 654 func drawText(ops *op.Ops, txt *text.Layout, face font.Face, fgcolor color.RGBA, pos, bounds image.Point, paintRect f32.Rectangle) { 655 clipObject := textPadding(txt.Lines) 656 clipObject.Max = clipObject.Max.Add(bounds) 657 658 var stack op.StackOp 659 stack.Push(ops) 660 paint.ColorOp{fgcolor}.Add(ops) 661 662 fc := fontFace2fontFace(&face) 663 664 for i := range txt.Lines { 665 txtstr, off, ok := clipLine(txt.Lines[i], clipObject) 666 if !ok { 667 continue 668 } 669 670 off.X += float32(pos.X) 671 off.Y += float32(pos.Y) + float32(i*FontHeight(face)) 672 673 var stack op.StackOp 674 stack.Push(ops) 675 676 op.TransformOp{}.Offset(off).Add(ops) 677 fc.shape(txtstr).Add(ops) 678 679 paint.PaintOp{Rect: paintRect.Sub(off)}.Add(ops) 680 681 stack.Pop() 682 } 683 684 stack.Pop() 685 } 686 687 type fontFace struct { 688 fnt *opentype.Font 689 shaper *text.Shaper 690 size int 691 fsize fixed.Int26_6 692 metrics ifont.Metrics 693 } 694 695 func fontFace2fontFace(f *font.Face) *fontFace { 696 return (*fontFace)(unsafe.Pointer(f)) 697 } 698 699 func (face *fontFace) layout(str string, width int) *text.Layout { 700 if width < 0 { 701 width = 1e6 702 } 703 return face.shaper.Layout(face, text.Font{}, str, text.LayoutOptions{MaxWidth: width}) 704 } 705 706 func (face *fontFace) shape(txtstr text.String) op.CallOp { 707 return face.shaper.Shape(face, text.Font{}, txtstr) 708 } 709 710 func (face *fontFace) Px(v unit.Value) int { 711 return face.size 712 } 713 714 func ChangeFontWidthCache(size int) { 715 } 716 717 func FontWidth(f font.Face, str string) int { 718 txt := fontFace2fontFace(&f).layout(str, -1) 719 maxw := 0 720 for i := range txt.Lines { 721 if w := txt.Lines[i].Width.Ceil(); w > maxw { 722 maxw = w 723 } 724 } 725 return maxw 726 } 727 728 func glyphAdvance(f font.Face, ch rune) int { 729 txt := fontFace2fontFace(&f).layout(string(ch), 1e6) 730 return txt.Lines[0].Width.Ceil() 731 } 732 733 func measureRunes(f font.Face, runes []rune) int { 734 text := fontFace2fontFace(&f).layout(string(runes), 1e6) 735 w := fixed.Int26_6(0) 736 for i := range text.Lines { 737 w += text.Lines[i].Width 738 } 739 return w.Ceil() 740 } 741 742 /////////////////////////////////////////////////////////////////////////////////// 743 // TEXT WIDGETS 744 /////////////////////////////////////////////////////////////////////////////////// 745 746 const ( 747 tabSizeInSpaces = 8 748 ) 749 750 type textWidget struct { 751 Padding image.Point 752 Background color.RGBA 753 Text color.RGBA 754 } 755 756 func widgetText(o *command.Buffer, b rect.Rect, str string, t *textWidget, a label.Align, f font.Face) { 757 b.H = max(b.H, 2*t.Padding.Y) 758 lblrect := rect.Rect{X: 0, W: 0, Y: b.Y + t.Padding.Y, H: b.H - 2*t.Padding.Y} 759 760 /* align in x-axis */ 761 switch a[0] { 762 case 'L': 763 lblrect.X = b.X + t.Padding.X 764 lblrect.W = max(0, b.W-2*t.Padding.X) 765 case 'C': 766 text_width := FontWidth(f, str) 767 text_width += (2.0 * t.Padding.X) 768 lblrect.W = max(1, 2*t.Padding.X+text_width) 769 lblrect.X = (b.X + t.Padding.X + ((b.W-2*t.Padding.X)-lblrect.W)/2) 770 lblrect.X = max(b.X+t.Padding.X, lblrect.X) 771 lblrect.W = min(b.X+b.W, lblrect.X+lblrect.W) 772 if lblrect.W >= lblrect.X { 773 lblrect.W -= lblrect.X 774 } 775 case 'R': 776 text_width := FontWidth(f, str) 777 text_width += (2.0 * t.Padding.X) 778 lblrect.X = max(b.X+t.Padding.X, (b.X+b.W)-(2*t.Padding.X+text_width)) 779 lblrect.W = text_width + 2*t.Padding.X 780 default: 781 panic("unsupported alignment") 782 } 783 784 /* align in y-axis */ 785 if len(a) >= 2 { 786 switch a[1] { 787 case 'C': 788 lblrect.Y = b.Y + b.H/2.0 - FontHeight(f)/2.0 789 case 'B': 790 lblrect.Y = b.Y + b.H - FontHeight(f) 791 } 792 } 793 if lblrect.H < FontHeight(f)*2 { 794 lblrect.H = FontHeight(f) * 2 795 } 796 797 o.DrawText(lblrect, str, f, t.Text) 798 } 799 800 func widgetTextWrap(o *command.Buffer, b rect.Rect, str []rune, t *textWidget, f font.Face) { 801 var text textWidget 802 803 text.Padding = image.Point{0, 0} 804 text.Background = t.Background 805 text.Text = t.Text 806 807 b.W = max(b.W, 2*t.Padding.X) 808 b.H = max(b.H, 2*t.Padding.Y) 809 b.H = b.H - 2*t.Padding.Y 810 811 var line rect.Rect 812 line.X = b.X + t.Padding.X 813 line.Y = b.Y + t.Padding.Y 814 line.W = b.W - 2*t.Padding.X 815 line.H = 2*t.Padding.Y + FontHeight(f) 816 817 lines := fontFace2fontFace(&f).layout(string(str), line.W) 818 819 for _, txtline := range lines.Lines { 820 if line.Y+line.H >= (b.Y + b.H) { 821 break 822 } 823 widgetText(o, line, txtline.Text.String, &text, "LC", f) 824 line.Y += FontHeight(f) + 2*t.Padding.Y 825 } 826 }