cell.go (6807B)
1 // Copyright 2024 The TCell Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use file except in compliance with the License. 5 // You may obtain a copy of the license at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package tcell 16 17 import ( 18 "os" 19 "reflect" 20 21 runewidth "github.com/mattn/go-runewidth" 22 ) 23 24 type cell struct { 25 currMain rune 26 currComb []rune 27 currStyle Style 28 lastMain rune 29 lastStyle Style 30 lastComb []rune 31 width int 32 lock bool 33 } 34 35 // CellBuffer represents a two-dimensional array of character cells. 36 // This is primarily intended for use by Screen implementors; it 37 // contains much of the common code they need. To create one, just 38 // declare a variable of its type; no explicit initialization is necessary. 39 // 40 // CellBuffer is not thread safe. 41 type CellBuffer struct { 42 w int 43 h int 44 cells []cell 45 } 46 47 // SetContent sets the contents (primary rune, combining runes, 48 // and style) for a cell at a given location. If the background or 49 // foreground of the style is set to ColorNone, then the respective 50 // color is left un changed. 51 func (cb *CellBuffer) SetContent(x int, y int, 52 mainc rune, combc []rune, style Style, 53 ) { 54 if x >= 0 && y >= 0 && x < cb.w && y < cb.h { 55 c := &cb.cells[(y*cb.w)+x] 56 57 // Wide characters: we want to mark the "wide" cells 58 // dirty as well as the base cell, to make sure we consider 59 // both cells as dirty together. We only need to do this 60 // if we're changing content 61 if (c.width > 0) && (mainc != c.currMain || len(combc) != len(c.currComb) || (len(combc) > 0 && !reflect.DeepEqual(combc, c.currComb))) { 62 for i := 0; i < c.width; i++ { 63 cb.SetDirty(x+i, y, true) 64 } 65 } 66 67 c.currComb = append([]rune{}, combc...) 68 69 if c.currMain != mainc { 70 c.width = runewidth.RuneWidth(mainc) 71 } 72 c.currMain = mainc 73 if style.fg == ColorNone { 74 style.fg = c.currStyle.fg 75 } 76 if style.bg == ColorNone { 77 style.bg = c.currStyle.bg 78 } 79 c.currStyle = style 80 } 81 } 82 83 // GetContent returns the contents of a character cell, including the 84 // primary rune, any combining character runes (which will usually be 85 // nil), the style, and the display width in cells. (The width can be 86 // either 1, normally, or 2 for East Asian full-width characters.) 87 func (cb *CellBuffer) GetContent(x, y int) (rune, []rune, Style, int) { 88 var mainc rune 89 var combc []rune 90 var style Style 91 var width int 92 if x >= 0 && y >= 0 && x < cb.w && y < cb.h { 93 c := &cb.cells[(y*cb.w)+x] 94 mainc, combc, style = c.currMain, c.currComb, c.currStyle 95 if width = c.width; width == 0 || mainc < ' ' { 96 width = 1 97 mainc = ' ' 98 } 99 } 100 return mainc, combc, style, width 101 } 102 103 // Size returns the (width, height) in cells of the buffer. 104 func (cb *CellBuffer) Size() (int, int) { 105 return cb.w, cb.h 106 } 107 108 // Invalidate marks all characters within the buffer as dirty. 109 func (cb *CellBuffer) Invalidate() { 110 for i := range cb.cells { 111 cb.cells[i].lastMain = rune(0) 112 } 113 } 114 115 // Dirty checks if a character at the given location needs to be 116 // refreshed on the physical display. This returns true if the cell 117 // content is different since the last time it was marked clean. 118 func (cb *CellBuffer) Dirty(x, y int) bool { 119 if x >= 0 && y >= 0 && x < cb.w && y < cb.h { 120 c := &cb.cells[(y*cb.w)+x] 121 if c.lock { 122 return false 123 } 124 if c.lastMain == rune(0) { 125 return true 126 } 127 if c.lastMain != c.currMain { 128 return true 129 } 130 if c.lastStyle != c.currStyle { 131 return true 132 } 133 if len(c.lastComb) != len(c.currComb) { 134 return true 135 } 136 for i := range c.lastComb { 137 if c.lastComb[i] != c.currComb[i] { 138 return true 139 } 140 } 141 } 142 return false 143 } 144 145 // SetDirty is normally used to indicate that a cell has 146 // been displayed (in which case dirty is false), or to manually 147 // force a cell to be marked dirty. 148 func (cb *CellBuffer) SetDirty(x, y int, dirty bool) { 149 if x >= 0 && y >= 0 && x < cb.w && y < cb.h { 150 c := &cb.cells[(y*cb.w)+x] 151 if dirty { 152 c.lastMain = rune(0) 153 } else { 154 if c.currMain == rune(0) { 155 c.currMain = ' ' 156 } 157 c.lastMain = c.currMain 158 c.lastComb = c.currComb 159 c.lastStyle = c.currStyle 160 } 161 } 162 } 163 164 // LockCell locks a cell from being drawn, effectively marking it "clean" until 165 // the lock is removed. This can be used to prevent tcell from drawing a given 166 // cell, even if the underlying content has changed. For example, when drawing a 167 // sixel graphic directly to a TTY screen an implementer must lock the region 168 // underneath the graphic to prevent tcell from drawing on top of the graphic. 169 func (cb *CellBuffer) LockCell(x, y int) { 170 if x < 0 || y < 0 { 171 return 172 } 173 if x >= cb.w || y >= cb.h { 174 return 175 } 176 c := &cb.cells[(y*cb.w)+x] 177 c.lock = true 178 } 179 180 // UnlockCell removes a lock from the cell and marks it as dirty 181 func (cb *CellBuffer) UnlockCell(x, y int) { 182 if x < 0 || y < 0 { 183 return 184 } 185 if x >= cb.w || y >= cb.h { 186 return 187 } 188 c := &cb.cells[(y*cb.w)+x] 189 c.lock = false 190 cb.SetDirty(x, y, true) 191 } 192 193 // Resize is used to resize the cells array, with different dimensions, 194 // while preserving the original contents. The cells will be invalidated 195 // so that they can be redrawn. 196 func (cb *CellBuffer) Resize(w, h int) { 197 if cb.h == h && cb.w == w { 198 return 199 } 200 201 newc := make([]cell, w*h) 202 for y := 0; y < h && y < cb.h; y++ { 203 for x := 0; x < w && x < cb.w; x++ { 204 oc := &cb.cells[(y*cb.w)+x] 205 nc := &newc[(y*w)+x] 206 nc.currMain = oc.currMain 207 nc.currComb = oc.currComb 208 nc.currStyle = oc.currStyle 209 nc.width = oc.width 210 nc.lastMain = rune(0) 211 } 212 } 213 cb.cells = newc 214 cb.h = h 215 cb.w = w 216 } 217 218 // Fill fills the entire cell buffer array with the specified character 219 // and style. Normally choose ' ' to clear the screen. This API doesn't 220 // support combining characters, or characters with a width larger than one. 221 // If either the foreground or background are ColorNone, then the respective 222 // color is unchanged. 223 func (cb *CellBuffer) Fill(r rune, style Style) { 224 for i := range cb.cells { 225 c := &cb.cells[i] 226 c.currMain = r 227 c.currComb = nil 228 cs := style 229 if cs.fg == ColorNone { 230 cs.fg = c.currStyle.fg 231 } 232 if cs.bg == ColorNone { 233 cs.bg = c.currStyle.bg 234 } 235 c.currStyle = cs 236 c.width = 1 237 } 238 } 239 240 var runeConfig *runewidth.Condition 241 242 func init() { 243 // The defaults for the runewidth package are poorly chosen for terminal 244 // applications. We however will honor the setting in the environment if 245 // it is set. 246 if os.Getenv("RUNEWIDTH_EASTASIAN") == "" { 247 runewidth.DefaultCondition.EastAsianWidth = false 248 } 249 }