gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

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 }