1 // Copyright 2023 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 views 16 17 import ( 18 "github.com/gdamore/tcell/v2" 19 ) 20 21 // View represents a logical view on an area. It will have some underlying 22 // physical area as well, generally. Views are operated on by Widgets. 23 type View interface { 24 // SetContent is used to update the content of the View at the given 25 // location. This will generally be called by the Draw() method of 26 // a Widget. 27 SetContent(x int, y int, ch rune, comb []rune, style tcell.Style) 28 29 // Size represents the visible size. The actual content may be 30 // larger or smaller. 31 Size() (int, int) 32 33 // Resize tells the View that its visible dimensions have changed. 34 // It also tells it that it has a new offset relative to any parent 35 // view. 36 Resize(x, y, width, height int) 37 38 // Fill fills the displayed content with the given rune and style. 39 Fill(rune, tcell.Style) 40 41 // Clear clears the content. Often just Fill(' ', tcell.StyleDefault) 42 Clear() 43 } 44 45 // ViewPort is an implementation of a View, that provides a smaller logical 46 // view of larger content area. For example, a scrollable window of text, 47 // the visible window would be the ViewPort, on the underlying content. 48 // ViewPorts have a two dimensional size, and a two dimensional offset. 49 // 50 // In some ways, as the underlying content is not kept persistently by the 51 // view port, it can be thought perhaps a little more precisely as a clipping 52 // region. 53 type ViewPort struct { 54 physx int // Anchor to the real world, usually 0 55 physy int // Again, anchor to the real world, usually 3 56 viewx int // Logical offset of the view 57 viewy int // Logical offset of the view 58 limx int // Content limits -- can't right scroll past this 59 limy int // Content limits -- can't down scroll past this 60 width int // View width 61 height int // View height 62 locked bool // if true, don't autogrow 63 v View 64 } 65 66 // Clear clears the displayed content, filling it with spaces of default 67 // text attributes. 68 func (v *ViewPort) Clear() { 69 v.Fill(' ', tcell.StyleDefault) 70 } 71 72 // Fill fills the displayed view port with the given character and style. 73 func (v *ViewPort) Fill(ch rune, style tcell.Style) { 74 if v.v != nil { 75 for y := 0; y < v.height; y++ { 76 for x := 0; x < v.width; x++ { 77 v.v.SetContent(x+v.physx, y+v.physy, ch, nil, style) 78 } 79 } 80 } 81 } 82 83 // Size returns the visible size of the ViewPort in character cells. 84 func (v *ViewPort) Size() (int, int) { 85 return v.width, v.height 86 } 87 88 // Reset resets the record of content, and also resets the offset back 89 // to the origin. It doesn't alter the dimensions of the view port, nor 90 // the physical location relative to its parent. 91 func (v *ViewPort) Reset() { 92 v.limx = 0 93 v.limy = 0 94 v.viewx = 0 95 v.viewy = 0 96 } 97 98 // SetContent is used to place data at the given cell location. Note that 99 // since the ViewPort doesn't retain this data, if the location is outside 100 // of the visible area, it is simply discarded. 101 // 102 // Generally, this is called during the Draw() phase by the object that 103 // represents the content. 104 func (v *ViewPort) SetContent(x, y int, ch rune, comb []rune, s tcell.Style) { 105 if v.v == nil { 106 return 107 } 108 if x > v.limx && !v.locked { 109 v.limx = x 110 } 111 if y > v.limy && !v.locked { 112 v.limy = y 113 } 114 if x < v.viewx || y < v.viewy { 115 return 116 } 117 if x >= (v.viewx + v.width) { 118 return 119 } 120 if y >= (v.viewy + v.height) { 121 return 122 } 123 v.v.SetContent(x-v.viewx+v.physx, y-v.viewy+v.physy, ch, comb, s) 124 } 125 126 // MakeVisible moves the ViewPort the minimum necessary to make the given 127 // point visible. This should be called before any content is changed with 128 // SetContent, since otherwise it may be possible to move the location onto 129 // a region whose contents have been discarded. 130 func (v *ViewPort) MakeVisible(x, y int) { 131 if x < v.limx && x >= v.viewx+v.width { 132 v.viewx = x - (v.width - 1) 133 } 134 if x >= 0 && x < v.viewx { 135 v.viewx = x 136 } 137 if y < v.limy && y >= v.viewy+v.height { 138 v.viewy = y - (v.height - 1) 139 } 140 if y >= 0 && y < v.viewy { 141 v.viewy = y 142 } 143 v.ValidateView() 144 } 145 146 // ValidateViewY ensures that the Y offset of the view port is limited so that 147 // it cannot scroll away from the content. 148 func (v *ViewPort) ValidateViewY() { 149 if v.viewy > v.limy-v.height { 150 v.viewy = v.limy - v.height 151 } 152 if v.viewy < 0 { 153 v.viewy = 0 154 } 155 } 156 157 // ValidateViewX ensures that the X offset of the view port is limited so that 158 // it cannot scroll away from the content. 159 func (v *ViewPort) ValidateViewX() { 160 if v.viewx > v.limx-v.width { 161 v.viewx = v.limx - v.width 162 } 163 if v.viewx < 0 { 164 v.viewx = 0 165 } 166 } 167 168 // ValidateView does both ValidateViewX and ValidateViewY, ensuring both 169 // offsets are valid. 170 func (v *ViewPort) ValidateView() { 171 v.ValidateViewX() 172 v.ValidateViewY() 173 } 174 175 // Center centers the point, if possible, in the View. 176 func (v *ViewPort) Center(x, y int) { 177 if x < 0 || y < 0 || x >= v.limx || y >= v.limy || v.v == nil { 178 return 179 } 180 v.viewx = x - (v.width / 2) 181 v.viewy = y - (v.height / 2) 182 v.ValidateView() 183 } 184 185 // ScrollUp moves the view up, showing lower numbered rows of content. 186 func (v *ViewPort) ScrollUp(rows int) { 187 v.viewy -= rows 188 v.ValidateViewY() 189 } 190 191 // ScrollDown moves the view down, showingh higher numbered rows of content. 192 func (v *ViewPort) ScrollDown(rows int) { 193 v.viewy += rows 194 v.ValidateViewY() 195 } 196 197 // ScrollLeft moves the view to the left. 198 func (v *ViewPort) ScrollLeft(cols int) { 199 v.viewx -= cols 200 v.ValidateViewX() 201 } 202 203 // ScrollRight moves the view to the left. 204 func (v *ViewPort) ScrollRight(cols int) { 205 v.viewx += cols 206 v.ValidateViewX() 207 } 208 209 // SetSize is used to set the visible size of the view. Enclosing views or 210 // layout managers can use this to inform the View of its correct visible size. 211 func (v *ViewPort) SetSize(width, height int) { 212 v.height = height 213 v.width = width 214 v.ValidateView() 215 } 216 217 // GetVisible returns the upper left and lower right coordinates of the visible 218 // content. That is, it will return x1, y1, x2, y2 where the upper left cell 219 // is position x1, y1, and the lower right is x2, y2. These coordinates are 220 // in the space of the content, that is the content area uses coordinate 0,0 221 // as its first cell position. 222 func (v *ViewPort) GetVisible() (int, int, int, int) { 223 return v.viewx, v.viewy, v.viewx + v.width - 1, v.viewy + v.height - 1 224 } 225 226 // GetPhysical returns the upper left and lower right coordinates of the visible 227 // content in the coordinate space of the parent. This is may be the physical 228 // coordinates of the screen, if the screen is the view's parent. 229 func (v *ViewPort) GetPhysical() (int, int, int, int) { 230 return v.physx, v.physy, v.physx + v.width - 1, v.physy + v.height - 1 231 } 232 233 // SetContentSize sets the size of the content area; this is used to limit 234 // scrolling and view moment. If locked is true, then the content size will 235 // not automatically grow even if content is placed outside of this area 236 // with the SetContent() method. If false, and content is drawn outside 237 // of the existing size, then the size will automatically grow to include 238 // the new content. 239 func (v *ViewPort) SetContentSize(width, height int, locked bool) { 240 v.limx = width 241 v.limy = height 242 v.locked = locked 243 v.ValidateView() 244 } 245 246 // GetContentSize returns the size of content as width, height in character 247 // cells. 248 func (v *ViewPort) GetContentSize() (int, int) { 249 return v.limx, v.limy 250 } 251 252 // Resize is called by the enclosing view to change the size of the ViewPort, 253 // usually in response to a window resize event. The x, y refer are the 254 // ViewPort's location relative to the parent View. A negative value for either 255 // width or height will cause the ViewPort to expand to fill to the end of parent 256 // View in the relevant dimension. 257 func (v *ViewPort) Resize(x, y, width, height int) { 258 if v.v == nil { 259 return 260 } 261 px, py := v.v.Size() 262 if x >= 0 && x < px { 263 v.physx = x 264 } 265 if y >= 0 && y < py { 266 v.physy = y 267 } 268 if width < 0 || width > px-x { 269 width = px - x 270 } 271 if height < 0 || height > py-y { 272 height = py - y 273 } 274 275 v.width = width 276 v.height = height 277 } 278 279 // SetView is called during setup, to provide the parent View. 280 func (v *ViewPort) SetView(view View) { 281 v.v = view 282 } 283 284 // NewViewPort returns a new ViewPort (and hence also a View). 285 // The x and y coordinates are an offset relative to the parent. 286 // The origin 0,0 represents the upper left. The width and height 287 // indicate a width and height. If the value -1 is supplied, then the 288 // dimension is calculated from the parent. 289 func NewViewPort(view View, x, y, width, height int) *ViewPort { 290 v := &ViewPort{v: view} 291 // initial (and possibly poor) assumptions -- all visible 292 // cells are addressible, but none beyond that. 293 v.limx = width 294 v.limy = height 295 v.Resize(x, y, width, height) 296 return v 297 } 298