2018-04-14 23:06:57 +01:00
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2018-04-07 14:24:55 +02:00
package draw
// text.go contains code that prints UTF-8 encoded strings on the canvas.
import (
"fmt"
"image"
2019-04-18 22:55:05 -04:00
"strings"
2018-04-07 14:24:55 +02:00
2019-02-24 01:13:26 -05:00
"github.com/mum4k/termdash/cell"
2020-04-10 15:26:45 -04:00
"github.com/mum4k/termdash/private/canvas"
"github.com/mum4k/termdash/private/runewidth"
2018-04-07 14:24:55 +02:00
)
// OverrunMode represents
type OverrunMode int
// String implements fmt.Stringer()
func ( om OverrunMode ) String ( ) string {
if n , ok := overrunModeNames [ om ] ; ok {
return n
}
return "OverrunModeUnknown"
}
// overrunModeNames maps OverrunMode values to human readable names.
var overrunModeNames = map [ OverrunMode ] string {
2018-05-06 19:28:52 +01:00
OverrunModeStrict : "OverrunModeStrict" ,
OverrunModeTrim : "OverrunModeTrim" ,
OverrunModeThreeDot : "OverrunModeThreeDot" ,
2018-04-07 14:24:55 +02:00
}
const (
// OverrunModeStrict verifies that the drawn value fits the canvas and
// returns an error if it doesn't.
OverrunModeStrict OverrunMode = iota
2018-05-06 19:28:52 +01:00
// OverrunModeTrim trims the part of the text that doesn't fit.
OverrunModeTrim
// OverrunModeThreeDot trims the text and places the horizontal ellipsis
// '…' character at the end.
OverrunModeThreeDot
2018-04-07 14:24:55 +02:00
)
2018-05-07 16:50:27 +01:00
// TextOption is used to provide options to Text().
type TextOption interface {
// set sets the provided option.
set ( * textOptions )
}
// textOptions stores the provided options.
type textOptions struct {
cellOpts [ ] cell . Option
maxX int
overrunMode OverrunMode
}
// textOption implements TextOption.
type textOption func ( * textOptions )
// set implements TextOption.set.
func ( to textOption ) set ( tOpts * textOptions ) {
to ( tOpts )
}
// TextCellOpts sets options on the cells that contain the text.
func TextCellOpts ( opts ... cell . Option ) TextOption {
return textOption ( func ( tOpts * textOptions ) {
tOpts . cellOpts = opts
} )
}
// TextMaxX sets a limit on the X coordinate (column) of the drawn text.
// The X coordinate of all cells used by the text must be within
// start.X <= X < TextMaxX.
// If not provided, the width of the canvas is used as TextMaxX.
func TextMaxX ( x int ) TextOption {
return textOption ( func ( tOpts * textOptions ) {
tOpts . maxX = x
} )
}
// TextOverrunMode indicates what to do with text that overruns the TextMaxX()
// or the width of the canvas if TextMaxX() isn't specified.
// Defaults to OverrunModeStrict.
func TextOverrunMode ( om OverrunMode ) TextOption {
return textOption ( func ( tOpts * textOptions ) {
tOpts . overrunMode = om
} )
2018-04-07 14:24:55 +02:00
}
2018-05-27 19:28:49 +01:00
// TrimText trims the provided text so that it fits the specified amount of cells.
func TrimText ( text string , maxCells int , om OverrunMode ) ( string , error ) {
2018-05-20 22:50:57 +01:00
if maxCells < 1 {
2018-05-27 19:28:49 +01:00
return "" , fmt . Errorf ( "maxCells(%d) cannot be less than one" , maxCells )
}
textCells := runewidth . StringWidth ( text )
if textCells <= maxCells {
// Nothing to do if the text fits.
return text , nil
}
switch om {
case OverrunModeStrict :
return "" , fmt . Errorf ( "the requested text %q takes %d cells to draw, space is available for only %d cells and overrun mode is %v" , text , textCells , maxCells , om )
case OverrunModeTrim , OverrunModeThreeDot :
default :
return "" , fmt . Errorf ( "unsupported overrun mode %d" , om )
2018-05-20 22:50:57 +01:00
}
2019-04-18 22:55:05 -04:00
var b strings . Builder
2018-05-27 19:28:49 +01:00
cur := 0
2018-05-20 22:50:57 +01:00
for _ , r := range text {
rw := runewidth . RuneWidth ( r )
2018-05-27 19:28:49 +01:00
if cur + rw >= maxCells {
2018-05-20 22:50:57 +01:00
switch {
2018-05-27 19:28:49 +01:00
case om == OverrunModeTrim :
// Only write the rune if it still fits, i.e. don't cut
2018-06-13 17:28:32 +01:00
// full-width runes in half.
2018-05-27 19:28:49 +01:00
if cur + rw == maxCells {
b . WriteRune ( r )
}
2018-05-20 22:50:57 +01:00
case om == OverrunModeThreeDot :
b . WriteRune ( '…' )
}
break
}
2018-05-06 19:28:52 +01:00
2018-05-27 19:28:49 +01:00
b . WriteRune ( r )
cur += rw
2018-05-06 19:28:52 +01:00
}
2018-05-27 19:28:49 +01:00
return b . String ( ) , nil
2018-05-06 19:28:52 +01:00
}
2018-05-07 16:50:27 +01:00
// Text prints the provided text on the canvas starting at the provided point.
2018-05-14 22:32:07 +01:00
func Text ( c * canvas . Canvas , text string , start image . Point , opts ... TextOption ) error {
2018-04-07 14:24:55 +02:00
ar := c . Area ( )
2018-05-07 16:50:27 +01:00
if ! start . In ( ar ) {
2018-05-14 22:32:07 +01:00
return fmt . Errorf ( "the requested start point %v falls outside of the provided canvas %v" , start , ar )
2018-05-07 16:50:27 +01:00
}
opt := & textOptions { }
for _ , o := range opts {
o . set ( opt )
2018-04-07 14:24:55 +02:00
}
2018-05-07 16:50:27 +01:00
if opt . maxX < 0 || opt . maxX > ar . Max . X {
2018-05-14 22:32:07 +01:00
return fmt . Errorf ( "invalid TextMaxX(%v), must be a positive number that is <= canvas.width %v" , opt . maxX , ar . Dx ( ) )
2018-04-07 14:24:55 +02:00
}
var wantMaxX int
2018-05-07 16:50:27 +01:00
if opt . maxX == 0 {
2018-04-07 14:24:55 +02:00
wantMaxX = ar . Max . X
} else {
2018-05-07 16:50:27 +01:00
wantMaxX = opt . maxX
2018-04-07 14:24:55 +02:00
}
2018-05-06 19:28:52 +01:00
2018-05-20 22:50:57 +01:00
maxCells := wantMaxX - start . X
2018-05-27 19:28:49 +01:00
trimmed , err := TrimText ( text , maxCells , opt . overrunMode )
2018-05-06 19:28:52 +01:00
if err != nil {
2018-05-14 22:32:07 +01:00
return err
2018-04-07 14:24:55 +02:00
}
2018-05-07 16:50:27 +01:00
cur := start
2018-05-06 19:28:52 +01:00
for _ , r := range trimmed {
2018-05-20 22:50:57 +01:00
cells , err := c . SetCell ( cur , r , opt . cellOpts ... )
if err != nil {
2018-05-14 22:32:07 +01:00
return err
2018-04-07 14:24:55 +02:00
}
2018-05-20 22:50:57 +01:00
cur = image . Point { cur . X + cells , cur . Y }
2018-04-07 14:24:55 +02:00
}
2018-05-14 22:32:07 +01:00
return nil
2018-04-07 14:24:55 +02:00
}
2019-01-26 23:58:38 -05:00
// ResizeNeeded draws an unicode character indicating that the canvas size is
// too small to draw meaningful content.
func ResizeNeeded ( cvs * canvas . Canvas ) error {
return Text ( cvs , "⇄" , image . Point { 0 , 0 } )
}