// Copyright 2016 The Tcell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use 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.
package ui
import (
"sync/atomic"
"unicode"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
)
// View provides a smaller logical view of a larger content area.
type View struct {
ScrollState
x int // Anchor to the real world
y int // Anchor to the real world
limx int // Content limits -- can't right scroll past this
limy int // Content limits -- can't down scroll past this
width int // View width
height int // View height
locked bool // if true, don't autogrow
v tcell.Screen
invalid *int32
exit *int32
}
// ScrollState contains the scroll state of the view.
type ScrollState struct {
scrollx int // Logical offset of the view
scrolly int // Logical offset of the view
}
// Clear clears the View.
func (v *View) Clear() {
v.Fill(' ', tcell.StyleDefault)
}
// Fill fills the View with the given character and style.
func (v *View) Fill(ch rune, style tcell.Style) {
for y := 0; y < v.height; y++ {
for x := 0; x < v.width; x++ {
v.v.SetContent(x+v.x, y+v.y, ch, nil, style)
}
}
}
// Size returns the visible size of the View in character cells.
func (v *View) Size() (int, int) {
return v.width, v.height
}
// Reset resets the record of content, and also resets the offset back
// to the origin. It doesn't alter the dimensions of the view port, nor
// the physical location relative to its parent.
func (v *View) Reset() {
v.limx = 0
v.limy = 0
v.scrollx = 0
v.scrolly = 0
}
// SetContent places data at the given cell location. Note that
// since the View doesn't retain this data, if the location is outside
// of the visible area, it is simply discarded.
func (v *View) SetContent(x, y int, ch rune, comb []rune, s tcell.Style) {
if x > v.limx && !v.locked {
v.limx = x
}
if y > v.limy && !v.locked {
v.limy = y
}
if x < v.scrollx || y < v.scrolly {
return
}
if x >= (v.scrollx + v.width) {
return
}
if y >= (v.scrolly + v.height) {
return
}
v.v.SetContent(x-v.scrollx+v.x, y-v.scrolly+v.y, ch, comb, s)
}
// MakeVisible moves the View the minimum amount necessary to make the given
// point visible. This should be called before any content is changed with
// SetContent, since otherwise it may be possible to move the location onto
// a region whose contents have been discarded.
func (v *View) MakeVisible(x, y int) {
if x < v.limx && x >= v.scrollx+v.width {
v.scrollx = x - (v.width - 1)
}
if x >= 0 && x < v.scrollx {
v.scrollx = x
}
if y < v.limy && y >= v.scrolly+v.height {
v.scrolly = y - (v.height - 1)
}
if y >= 0 && y < v.scrolly {
v.scrolly = y
}
v.Validate()
}
// Validate ensures that the X and Y offsets of the View are valid.
func (v *View) Validate() {
if v.scrollx >= v.limx-v.width {
v.scrollx = (v.limx - v.width)
}
if v.scrollx < 0 {
v.scrollx = 0
}
if v.scrolly >= v.limy-v.height {
v.scrolly = (v.limy - v.height)
}
if v.scrolly < 0 {
v.scrolly = 0
}
}
// Center centers the point, if possible, in the View.
func (v *View) Center(x, y int) {
if x < 0 || y < 0 || x >= v.limx || y >= v.limy || v.v == nil {
return
}
v.scrollx = x - (v.width / 2)
v.scrolly = y - (v.height / 2)
v.Validate()
}
// ScrollUp moves the view up, showing lower numbered rows of content.
func (v *View) ScrollUp(rows int) {
v.scrolly -= rows
v.Validate()
}
// ScrollDown moves the View down, showing higher numbered rows of content.
func (v *View) ScrollDown(rows int) {
v.scrolly += rows
v.Validate()
}
// ScrollLeft moves the View to the left.
func (v *View) ScrollLeft(cols int) {
v.scrollx -= cols
v.Validate()
}
// ScrollRight moves the View to the left.
func (v *View) ScrollRight(cols int) {
v.scrollx += cols
v.Validate()
}
// ScrollX returns the scroll offset of the View in the x-direction.
func (v *View) ScrollX() int {
return v.scrollx
}
// ScrollY returns the scroll offset of the View in the y-direction.
func (v *View) ScrollY() int {
return v.scrolly
}
// SetContentSize sets the size of the content area; this is used to limit
// scrolling and view moment. If locked is true, then the content size will
// not automatically grow even if content is placed outside of this area
// with the SetContent() method. If false, and content is drawn outside
// of the existing size, then the size will automatically grow to include
// the new content.
func (v *View) SetContentSize(width, height int, locked bool) {
v.limx = width
v.limy = height
v.locked = locked
v.Validate()
}
// GetContentSize returns the size of content as width, height in character
// cells.
func (v *View) GetContentSize() (int, int) {
return v.limx, v.limy
}
// Layout sets the position and size of the view, usually in response to a
// window resize event. (x, y) refer to the position of the view on the screen.
// A negative value for either width or height will cause the View to expand to
// fill to the end of the screen in the relevant dimension.
func (v *View) Layout(x, y, width, height int) {
px, py := v.v.Size()
if x >= 0 && x < px {
v.x = x
}
if y >= 0 && y < py {
v.y = y
}
if width < 0 {
width = px - x
}
if height < 0 {
height = py - y
}
if width <= x+px {
v.width = width
}
if height <= y+py {
v.height = height
}
}
// ShowCursor shows the cursor at (x, y).
func (v *View) ShowCursor(x, y int) {
v.v.ShowCursor(v.x+x, v.y+y)
}
// HideCursor hides the cursor.
func (v *View) HideCursor() {
v.v.HideCursor()
}
// DrawText draws the provided text at (sx, sy). It returns the number of cells used.
func (v *View) DrawText(sx, sy int, text string, style tcell.Style) int {
r := rune(0)
w := 0
x := 0
var comb []rune
for _, l := range text {
if l == '\t' {
if w != 0 {
v.SetContent(sx+x, sy, r, comb, style)
}
w = 0
x += 4
continue
}
if !unicode.IsGraphic(l) {
continue
}
if runewidth.RuneWidth(l) == 0 {
comb = append(comb, l)
continue
}
if w != 0 {
v.SetContent(sx+x, sy, r, comb, style)
x += w
}
r = l
w = runewidth.RuneWidth(l)
comb = nil
}
if w != 0 {
v.SetContent(sx+x, sy, r, comb, style)
x += w
}
return x
}
// Contains reports whether the view contains the point (x, y).
func (v *View) Contains(x, y int) bool {
w, h := v.Size()
return x >= v.x && x < v.x+w && y >= v.y && y < v.y+h
}
// GetLocal transforms (x, y) to local coordinates.
func (v *View) GetLocal(x, y int) (int, int) {
return x - v.x, y - v.y
}
// PgUp scrolls up one page.
func (v *View) PgUp() {
_, h := v.Size()
v.ScrollUp(h - 2)
}
// PgDn scrolls down one page.
func (v *View) PgDn() {
_, h := v.Size()
v.ScrollDown(h - 2)
}
// Top scrolls to the top of the view.
func (v *View) Top() {
v.MakeVisible(0, 0)
}
// Bottom scrolls to the bottom of the view.
func (v *View) Bottom() {
_, h := v.GetContentSize()
_, vh := v.Size()
v.MakeVisible(0, h-vh)
}
// Invalidate invalidates the view.
func (v *View) Invalidate() {
atomic.StoreInt32(v.invalid, 1)
}
// Exit exits the UI.
func (v *View) Exit() {
atomic.StoreInt32(v.exit, 1)
}