// Package cpick is an interactive color picker in the terminal using cview package cpick import ( "encoding/json" "fmt" "io/ioutil" "log" "math" "os" "os/user" "regexp" "strconv" "strings" color "github.com/ethanbaker/colors" "github.com/ethanbaker/cpick/cview" "github.com/gdamore/tcell/v2" ) // Breakpoint boundaries const BREAKPOINT_HEIGHT = 30 const BREAKPOINT_WIDTH = 110 // Global configuration variables var testingMode = false var smallWidth = false var smallHeight = false // jsonColorInfo type used to hold imported colors type jsonColorInfo struct { name string length int colors []jsonColor table *cview.Table } // ColorValues type used to hold color values and optional name type ColorValues struct { RGB color.RGB HSV color.HSV HSL color.HSL CMYK color.CMYK Hex color.Hex Decimal color.Decimal Ansi color.Ansi Name string } var colorBlockWide string = ` ███████████████████ ███████████████████ ███████████████████ ███████████████████ ` var colorBlockSmall string = ` ████████████ ` var colorTextWide string = ` RGB: %v, %v, %v HSV: %v°, %v%%, %v%% HSL: %v°, %v%%, %v%% CMYK: %v%%, %v%%, %v%%, %v%% Hex: #%v Decimal: %v Ansi: "\033%v" ` var colorTextSmall string = ` RGB: %v,%v,%v HSV: %v°,%v%%,%v%% HSL: %v°,%v%%,%v%% CMYK: %v%%,%v%% %v%%,%v%% Hex: #%v Decimal: %v Ansi: "\033%v" ` var colorPageText string = "██████████ %v %v " var helpString string = ` Movement: vim keys (h,j,k,l) or arrow keys Quitting the application: escape or q While on the hue table: - Press enter to create a new saturation-value table - Press space to select the preset color table - Press tab to switch to the saturation-value table While on the preset color table: - Press enter to create a new saturation-value table - Press space to select the hue table - Press tab to switch to the saturation-value table - Press C to go to the next color page - Press c to go to the previous color page - Press ? to enter a search menu for colors - Press N to go to the next search instance - Press n to go to the previous search instance While on the saturation-value table: - Press enter to select the final color - Press tab to switch to the hue table ` var searchHelpString string = ` To search for a color name, type the name of the color into the search bar. Related colors will appear below. Once a color (or phrase) is desired, press enter. You can press N (forward) and n (reverse) to swap between instances. Each value type you want to select will have instructions below: - Hexadecimal: type the hex value starting with "#" (EX: #ffffff) - RGB: type "rgb:" and three RGB values separated by a space (EX: rgb: 255 255 255) - HSV: type "hsv:" and three HSV values separated by a space (EX: hsv: 0 100 0) - HSL: type "hsl:" and three HSL values separated by a space (EX: hsl: 0 100 50) - CMYK: type "cmyk:" and four CMYK values separated by a space (EX: cmyk: 0 0 0 0) - Decimal: type "decimal:" and then the decimal value (EX: 16777215) Once a color is selected, you will be taken to the Saturation-Value table with the specified color selected. Any errors that you make will appear in red below the search bar. ` // Global variables to make up elements on screen var app *cview.Application = cview.NewApplication() var pages *cview.Pages = cview.NewPages() var hFlex *cview.Flex = cview.NewFlex() var svFlex *cview.Flex = cview.NewFlex() var hTable *cview.Table = cview.NewTable() var svTable *cview.Table = cview.NewTable() var darkHBlock *cview.TextView = cview.NewTextView() var darkHText *cview.TextView = cview.NewTextView() var lightHBlock *cview.TextView = cview.NewTextView() var lightHText *cview.TextView = cview.NewTextView() var darkSVBlock *cview.TextView = cview.NewTextView() var darkSVText *cview.TextView = cview.NewTextView() var lightSVBlock *cview.TextView = cview.NewTextView() var lightSVText *cview.TextView = cview.NewTextView() var colorPageTitle *cview.TextView = cview.NewTextView() var jsonColors *cview.Flex = cview.NewFlex() var colorPages *cview.Pages = cview.NewPages() var colorPageIndex int var colorInfo []jsonColorInfo var helpFlex *cview.Flex = cview.NewFlex() var helpModal *cview.Modal = cview.NewModal() var helpFocus cview.Primitive = hTable var searchFlex *cview.Flex = cview.NewFlex() var searchStatus *cview.TextView = cview.NewTextView() var searchInput *cview.InputField = cview.NewInputField() var searchNames []string var searchIndexes [][]int var searchIndex int var hFocus cview.Primitive = hTable var hue int var returnColor ColorValues // Input Handlers --------------------------------------------------------- func inputCaptureHandler(event *tcell.EventKey) *tcell.EventKey { switch { case event.Rune() == 'q': if !searchFlex.HasFocus() { app.Stop() } case event.Rune() == '`': showHelp() case event.Key() == tcell.KeyCtrlF || event.Rune() == '?': showSearch() return nil } if svTable.HasFocus() { event = svCaptureHandler(event) } else if hTable.HasFocus() { event = hCaptureHandler(event) } else if colorPages.HasFocus() { event = colorPageCaptureHandler(event) } return event } func svCaptureHandler(event *tcell.EventKey) *tcell.EventKey { return event } func hCaptureHandler(event *tcell.EventKey) *tcell.EventKey { switch { case event.Rune() == ' ': hFocus = colorPages app.SetFocus(colorPages) row, col := colorInfo[colorPageIndex].table.GetSelection() text := colorInfo[colorPageIndex].table.GetCell(row, col).Text raw := strings.Split(string(text[:]), "#") hsv := color.HextoHSV(color.Hex(raw[1])) darkHSV := hsv lightHSV := hsv if hsv.V%2 == 0 { darkHSV.V -= 1 } else { lightHSV.V += 1 } setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText) colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone) } return event } func colorPageCaptureHandler(event *tcell.EventKey) *tcell.EventKey { switch { // Change pages of color tables case event.Rune() == 'C': if colorPageIndex < len(colorInfo)-1 { colorPageIndex++ colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone) name := colorInfo[colorPageIndex].name colorPageTitle.SetText(name) pageId := fmt.Sprintf("page-%d", colorPageIndex) colorPages.SwitchToPage(pageId) row, col := colorInfo[colorPageIndex].table.GetSelection() text := string(colorInfo[colorPageIndex].table.GetCell(row, col).Text[:]) raw := strings.Split(text, "#") hsv := color.HextoHSV(color.Hex(raw[1])) darkHSV := hsv lightHSV := hsv if hsv.V%2 == 0 { darkHSV.V -= 1 } else { lightHSV.V += 1 } setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText) } case event.Rune() == 'c': if colorPageIndex > 0 { colorPageIndex-- colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone) name := colorInfo[colorPageIndex].name colorPageTitle.SetText(name) pageId := fmt.Sprintf("page-%d", colorPageIndex) colorPages.SwitchToPage(pageId) row, col := colorInfo[colorPageIndex].table.GetSelection() text := string(colorInfo[colorPageIndex].table.GetCell(row, col).Text[:]) raw := strings.Split(text, "#") hsv := color.HextoHSV(color.Hex(raw[1])) darkHSV := hsv lightHSV := hsv if hsv.V%2 == 0 { darkHSV.V -= 1 } else { lightHSV.V += 1 } setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText) } // Switch to hTable case event.Rune() == ' ': hFocus = hTable app.SetFocus(hTable) _, col := hTable.GetSelection() darkHSV := color.HSV{H: col * 2, S: 100, V: 100} lightHSV := color.HSV{H: col*2 + 1, S: 100, V: 100} setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText) colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, tcell.AttrBold) } searchInputCaptureHandler(event) return colorPageMovementHandler(event) } // Handle any movement events by preventing the user from selecting // a blank filler cell func colorPageMovementHandler(event *tcell.EventKey) *tcell.EventKey { switch { case event.Rune() == 'l' || event.Key() == tcell.KeyRight: row, col := colorInfo[colorPageIndex].table.GetSelection() if col < colorInfo[colorPageIndex].table.GetColumnCount()-1 { cell := colorInfo[colorPageIndex].table.GetCell(row, col+1) if len(cell.Text) == 0 { return nil } } case event.Rune() == 'j' || event.Key() == tcell.KeyDown: row, col := colorInfo[colorPageIndex].table.GetSelection() if row < colorInfo[colorPageIndex].table.GetRowCount()-1 { cell := colorInfo[colorPageIndex].table.GetCell(row+1, col) if len(cell.Text) == 0 { return nil } } case event.Rune() == 'G': row := colorInfo[colorPageIndex].table.GetRowCount() - 1 col := colorInfo[colorPageIndex].table.GetColumnCount() - 1 for true { cell := colorInfo[colorPageIndex].table.GetCell(row, col) if len(cell.Text) != 0 { break } else { row-- } } colorInfo[colorPageIndex].table.Select(row, col) return nil } return event } func searchInputCaptureHandler(event *tcell.EventKey) *tcell.EventKey { if len(searchIndexes) > 1 { switch event.Rune() { // Go back a selection case 'n': if searchIndex == 0 { searchIndex = len(searchIndexes) } searchIndex-- colorPages.SwitchToPage(fmt.Sprintf("page-%v", searchIndexes[searchIndex][0])) colorPageIndex = searchIndexes[searchIndex][0] colorInfo[colorPageIndex].table.Select(searchIndexes[searchIndex][2], searchIndexes[searchIndex][1]) colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone) // Go forward a selection case 'N': if searchIndex == len(searchIndexes)-1 { searchIndex = -1 } searchIndex++ colorPages.SwitchToPage(fmt.Sprintf("page-%v", searchIndexes[searchIndex][0])) colorPageIndex = searchIndexes[searchIndex][0] colorInfo[colorPageIndex].table.Select(searchIndexes[searchIndex][2], searchIndexes[searchIndex][1]) colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone) } } return event } // Screen setup ----------------------------------------------------------- func hScreenSetup() { // Dark color value setup darkText := cview.NewTextView() darkText.SetScrollBarVisibility(cview.ScrollBarNever) if !smallWidth && !smallHeight { darkText.SetText("Dark Tint Color") darkHBlock.SetText(colorBlockWide) } else { darkText.SetText("Color") darkHBlock.SetText(colorBlockSmall) } darkHBlock.SetScrollBarVisibility(cview.ScrollBarNever) darkHText.SetScrollBarVisibility(cview.ScrollBarNever) darkColorFlex := cview.NewFlex() darkColorFlex.SetDirection(cview.FlexRow) darkColorFlex.AddItem(darkText, 0, 1, false) darkColorFlex.AddItem(darkHBlock, 0, 2, false) darkColorFlex.AddItem(darkHText, 0, 9, false) // Light color value setup lightText := cview.NewTextView() lightColorFlex := cview.NewFlex() if !smallHeight && !smallWidth { lightText.SetScrollBarVisibility(cview.ScrollBarNever) lightText.SetText(" Light Tint Color") lightHBlock.SetText(colorBlockWide) lightHBlock.SetScrollBarVisibility(cview.ScrollBarNever) lightHText.SetScrollBarVisibility(cview.ScrollBarNever) lightColorFlex.SetDirection(cview.FlexRow) lightColorFlex.AddItem(lightText, 0, 1, false) lightColorFlex.AddItem(lightHBlock, 0, 2, false) lightColorFlex.AddItem(lightHText, 0, 9, false) } colorFlex := cview.NewFlex() colorFlex.SetDirection(cview.FlexRow) colorFlex.AddItem(darkColorFlex, 0, 1, false) if !smallHeight && !smallWidth { colorFlex.AddItem(lightColorFlex, 0, 1, false) } // Everything except hTable setup lowerFlex := cview.NewFlex() lowerFlex.SetDirection(cview.FlexColumn) lowerFlex.AddItem(colorFlex, 0, 3, false) lowerFlex.AddItem(jsonColors, 0, 9, false) help := cview.NewTextView() help.SetTextAlign(cview.AlignRight) help.SetText("Press ` to see help") hFlex.SetDirection(cview.FlexRow) hFlex.AddItem(hTable, 0, 1, true) hFlex.AddItem(help, 0, 1, false) hFlex.AddItem(lowerFlex, 0, 20, false) darkHSV := color.HSV{H: 0, S: 100, V: 100} lightHSV := color.HSV{H: 0, S: 100, V: 100} setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText) } func svScreenSetup() { // Fill the text with the default values darkSVBlock.SetScrollBarVisibility(cview.ScrollBarNever) lightSVBlock.SetScrollBarVisibility(cview.ScrollBarNever) if !smallWidth && !smallHeight { darkSVBlock.SetText(colorBlockWide) lightSVBlock.SetText(colorBlockWide) } else { darkSVBlock.SetText(colorBlockSmall) } darkHSV := color.HSV{H: 0, S: 100, V: 99} lightHSV := color.HSV{H: 0, S: 100, V: 100} setColorValues(darkHSV, darkSVBlock, darkSVText, lightHSV, lightSVBlock, lightSVText) // Setup the screen darkTitle := cview.NewTextView() if !smallWidth && !smallHeight { darkTitle.SetText(" Dark Tint Color") } else { darkTitle.SetText(" Color") } darkSVText.SetScrollBarVisibility(cview.ScrollBarNever) darkSVFlex := cview.NewFlex() darkSVFlex.SetDirection(cview.FlexRow) darkSVFlex.AddItem(darkTitle, 0, 1, false) darkSVFlex.AddItem(darkSVBlock, 0, 2, false) darkSVFlex.AddItem(darkSVText, 0, 9, false) lightTitle := cview.NewTextView() lightSVFlex := cview.NewFlex() if !smallHeight && !smallWidth { lightTitle.SetText(" Light Tint Color") lightSVText.SetScrollBarVisibility(cview.ScrollBarNever) lightSVFlex.SetDirection(cview.FlexRow) lightSVFlex.AddItem(lightTitle, 0, 1, false) lightSVFlex.AddItem(lightSVBlock, 0, 2, false) lightSVFlex.AddItem(lightSVText, 0, 9, false) } colorFlex := cview.NewFlex() colorFlex.SetDirection(cview.FlexRow) colorFlex.AddItem(darkSVFlex, 0, 1, false) if !smallHeight && !smallWidth { colorFlex.AddItem(lightSVFlex, 0, 1, false) } svFlex.AddItem(svTable, 0, 4, false) svFlex.AddItem(colorFlex, 0, 1, false) } // Help page setup -------------------------------------------------------- func helpPageSetup() { helpModal.SetText(helpString) helpModal.AddButtons([]string{"Exit help"}) helpModal.SetDoneFunc(helpModalDoneFunc) helpFlex.AddItem(helpModal, 0, 1, false) } func helpModalDoneFunc(buttonIndex int, buttonLabel string) { if buttonLabel == "Exit help" { if helpFocus == hTable || helpFocus == colorPages { hFlex.RemoveItem(helpFlex) } else if helpFocus == svTable { svFlex.RemoveItem(helpFlex) svFlex.SetDirection(cview.FlexColumn) } app.SetFocus(helpFocus) } } // Search page setup ------------------------------------------------------ func searchInputSetup() { for i := 0; i < len(colorInfo); i++ { for _, c := range colorInfo[i].colors { searchNames = append(searchNames, strings.ToLower(c.NAME)) } } searchInput.SetLabel("Enter a color name or value to search for: ") searchInput.SetFieldWidth(60) searchInput.SetDoneFunc(searchInputDoneFunc) searchInput.SetAutocompleteFunc(searchInputAutocompleteFunc) searchStatus.SetTextColor(tcell.ColorRed) searchHelp := cview.NewTextView() searchHelp.SetText(searchHelpString) searchFlex.SetDirection(cview.FlexRow) searchFlex.AddItem(searchInput, 0, 1, false) searchFlex.AddItem(searchStatus, 0, 1, false) searchFlex.AddItem(searchHelp, 0, 4, false) } func searchInputDoneFunc(key tcell.Key) { switch key { // Go back to the main application case tcell.KeyEscape: pages.SwitchToPage("Hue page") colorInfo[colorPageIndex].table.Select(0, 0) app.SetFocus(colorPages) // Select a value on the color tables case tcell.KeyEnter: text := strings.ToLower(strings.TrimSpace(searchInput.GetText())) if len(text) > 0 { parseSearchText(text) } } } func parseSearchText(text string) { raw := strings.Split(strings.TrimSpace(strings.Join(strings.Split(text, ":")[1:], "")), " ") var ints []int safe := true for _, v := range raw { num, err := strconv.ParseInt(v, 10, 64) if err != nil { safe = false break } ints = append(ints, int(num)) } if len(text) > 5 && text[0:5] == "ansi:" { searchStatus.SetText("Please enter the RGB values inside of the ansi escape sequence") return } else if !safe && text[0] != '#' && strings.Contains(text, ":") { searchStatus.SetText("Please enter valid numbers") return } var hsv color.HSV var statusMessage string switch { case strings.HasPrefix(strings.ToLower(text), strings.ToLower("#")): matched, err := regexp.MatchString(`[0-9a-fA-F]+`, text) testErr(err) if matched && len(text) == 7 { hsv = color.HextoHSV(color.Hex(text)) } else { statusMessage = "Please enter a valid hexadecimal value" } case strings.HasPrefix(strings.ToLower(text), strings.ToLower("rgb:")): for _, v := range ints { if v > 255 || v < 0 { statusMessage = "Please enter valid RGB values (0 < x < 255)" } } if len(ints) == 3 { rgb := color.RGB{R: ints[0], G: ints[1], B: ints[2]} hsv = color.RGBtoHSV(rgb) } else { statusMessage = "Please enter 3 RGB values" } case strings.HasPrefix(strings.ToLower(text), strings.ToLower("hsv:")): if ints[0] < 0 || ints[0] > 359 { statusMessage = "Please enter a valid hue value (0 < x < 359)" } for _, v := range ints[1:] { if v > 100 || v < 0 { statusMessage = "Please enter valid Saturation and Value values (0 < x < 100)" } } if len(ints) == 3 { hsv = color.HSV{H: ints[0], S: ints[1], V: ints[2]} } else { statusMessage = "Please enter 3 HSV values" } case strings.HasPrefix(strings.ToLower(text), strings.ToLower("hsl:")): if ints[0] < 0 || ints[0] > 359 { statusMessage = "Please enter a valid hue value (0 < x < 359)" } for _, v := range ints[1:] { if v > 100 || v < 0 { statusMessage = "Please enter valid Saturation and Length values (0 < x < 100)" } } if len(ints) == 3 { hsl := color.HSL{H: ints[0], S: ints[1], L: ints[2]} hsv = color.HSLtoHSV(hsl) } else { statusMessage = "Please enter 3 HSL values" } case strings.HasPrefix(strings.ToLower(text), strings.ToLower("cmyk:")): for _, v := range ints[1:] { if v > 100 || v < 0 { statusMessage = "Please enter valid CMYK values (0 < x < 100)" } } if len(ints) == 4 { cmyk := color.CMYK{C: ints[0], M: ints[1], Y: ints[2], K: ints[3]} hsv = color.CMYKtoHSV(cmyk) } else { statusMessage = "Please enter 4 CMYK values" } case strings.HasPrefix(strings.ToLower(text), strings.ToLower("decimal:")): if ints[0] < 0 || ints[0] > 16777215 { statusMessage = "Please enter a valid decimal value (0 < x < 16777215)" } if len(ints) == 1 { decimal := ints[0] hsv = color.DecimaltoHSV(color.Decimal(decimal)) } else { statusMessage = "Please enter 1 decimal value" } default: locations := getColorLocations(text) searchIndexes = locations pages.SwitchToPage("Hue page") app.SetFocus(colorPages) if len(locations) > 0 { colorPages.SwitchToPage(fmt.Sprintf("page-%v", locations[0][0])) colorPageIndex = locations[0][0] colorInfo[colorPageIndex].table.Select(locations[0][2], locations[0][1]) colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone) } searchInput.SetText("") return } if statusMessage != "" { searchStatus.SetText(statusMessage) return } hue = hsv.H drawSVTable() svTable.Select(int(math.Round(50-float64(hsv.V/2))), hsv.S) pages.SwitchToPage("Saturation-Value page") app.SetFocus(svTable) searchInput.SetText("") searchStatus.SetText("") return } func searchInputAutocompleteFunc(currentText string) []*cview.ListItem { if len(currentText) == 0 { return nil } // Create a list of possible selections var entries []string for _, word := range searchNames { if strings.HasPrefix(strings.ToLower(word), strings.ToLower(currentText)) { entries = append(entries, word) } } // If the list is 0 or there is only one option that the user already // typed if len(entries) < 1 { entries = nil } else if len(entries) == 1 && currentText == entries[0] { entries = nil } // Convert strings to list items var items []*cview.ListItem for _, v := range entries { items = append(items, cview.NewListItem(v)) } return items } // Color pages setup ------------------------------------------------------ func colorPageSetup() { colorInfo = make([]jsonColorInfo, 0) path, err := getPath() testErr(err) var data jsonData data = getCustomColors(path) // Get the lists of all of the imported colors for i := 0; i < len(data.COLORLIST); i++ { c := jsonColorInfo{} colorInfo = append(colorInfo, c) colorInfo[i].name = strings.Title(data.COLORLIST[i].NAME + " pages") colorInfo[i].length = len(data.COLORLIST[i].COLORS) colorInfo[i].colors = data.COLORLIST[i].COLORS for j := 0; j < 8; j++ { colorInfo[i].colors = append(colorInfo[i].colors, jsonColor{"", "000000"}) } colorInfo[i].table = cview.NewTable() colorInfo[i].table.SetCellPadding(3, 0) colorInfo[i].table.SetScrollBarVisibility(cview.ScrollBarNever) colorInfo[i].table.SetSelectable(true, true) colorInfo[i].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, tcell.AttrBold) colorInfo[i].table.SetDoneFunc(colorPageDoneFunc) colorInfo[i].table.SetSelectedFunc(colorPageSelectedFunc) colorInfo[i].table.SetSelectionChangedFunc(colorPageSelectionChangedFunc) } // Make pages to hold the tables for all of the colors for colorIndex := 0; colorIndex < len(colorInfo); colorIndex++ { for x := 0; x < int(math.Ceil(float64(colorInfo[colorIndex].length/9)))+1; x++ { for y := 0; y < 9; y++ { rgb := color.HextoRGB(color.Hex(colorInfo[colorIndex].colors[x*9+y].VALUE)) name := strings.ToLower(colorInfo[colorIndex].colors[x*9+y].NAME) val := strings.ToLower(colorInfo[colorIndex].colors[x*9+y].VALUE) // Draw the color if it can actually be seen if colorInfo[colorIndex].colors[x*9+y].NAME == "" { cell := cview.NewTableCell("") cell.SetTextColor(0) colorInfo[colorIndex].table.SetCell(y, x, cell) } else if rgb.R+rgb.G+rgb.B > 84 { text := fmt.Sprintf(colorPageText, name, val) c := tcell.NewHexColor(int32(color.HextoDecimal(color.Hex(val)))) cell := cview.NewTableCell(text) cell.SetTextColor(c) colorInfo[colorIndex].table.SetCell(y, x, cell) } else { text := fmt.Sprintf("██████████ [white]%v %v ", name, val) c := tcell.NewHexColor(int32(color.HextoDecimal(color.Hex(val)))) cell := cview.NewTableCell(text) cell.SetTextColor(c) colorInfo[colorIndex].table.SetCell(y, x, cell) } } } pageId := fmt.Sprintf("page-%d", colorIndex) colorPages.AddPage(pageId, colorInfo[colorIndex].table, true, false) } colorPages.SwitchToPage("page-0") colorPageTitle.SetTextAlign(cview.AlignCenter) colorPageTitle.SetText(strings.Title(colorInfo[0].name)) // Setup the color page jsonColors.SetDirection(cview.FlexRow) jsonColors.AddItem(colorPageTitle, 0, 1, false) jsonColors.AddItem(colorPages, 0, 10, false) } func colorPageDoneFunc(key tcell.Key) { switch { case key == tcell.KeyEscape: app.Stop() case key == tcell.KeyTab: pages.SwitchToPage("Saturation-Value page") app.SetFocus(svTable) } } func colorPageSelectedFunc(row int, column int) { // Switch to blank saturation-value page svTable.ScrollToBeginning() svTable.Clear() pages.SwitchToPage("Saturation-Value page") app.SetFocus(svTable) // Get the color displayed in the table text := colorInfo[colorPageIndex].table.GetCell(row, column).Text raw := strings.Split(string(text[:]), "#") hsv := color.HextoHSV(color.Hex(raw[1])) cursor := color.HSVtoRGB(color.HSV{H: (hsv.H + 180) % 360, S: 100, V: 100}) c := tcell.NewRGBColor(int32(cursor.R), int32(cursor.G), int32(cursor.B)) svTable.SetSelectedStyle(c, c, tcell.AttrNone) hue = hsv.H drawSVTable() // Move the user to the selected color x := hsv.S y := 50 - hsv.V/2 if hsv.V%2 == 1 { y-- } svTable.Select(y, x) } func colorPageSelectionChangedFunc(row int, column int) { // Get the color from the table text := colorInfo[colorPageIndex].table.GetCell(row, column).Text raw := strings.Split(string(text[:]), "#") hsv := color.HextoHSV(color.Hex(raw[1])) // Fill the color format string with the correct values for the selected // color darkHSV := hsv lightHSV := hsv if hsv.V%2 == 0 { darkHSV.V -= 1 } else { lightHSV.V += 1 } setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText) } // hTable setup ---------------------------------------------------------- func hTableSetup() { // Set the hue table with its necessary properties hTable.SetSelectable(true, true) hTable.Select(0, 0) hTable.SetSelectedStyle(tcell.ColorWhite, tcell.ColorWhite, tcell.AttrNone) hTable.SetCellPadding(0, 0) // Color the hue table for h := 0; h < 360; h += 2 { bg := color.HSVtoRGB(color.HSV{H: h + 1, S: 100, V: 100}) fg := color.HSVtoRGB(color.HSV{H: h, S: 100, V: 100}) bc := tcell.NewRGBColor(int32(bg.R), int32(bg.G), int32(bg.B)) c := tcell.NewRGBColor(int32(fg.R), int32(fg.G), int32(fg.B)) cell := cview.NewTableCell("▐") cell.SetBackgroundColor(bc) cell.SetTextColor(c) hTable.SetCell(0, h/2, cell) } hTable.SetDoneFunc(hTableDoneFunc) hTable.SetSelectedFunc(hTableSelectedFunc) hTable.SetSelectionChangedFunc(hTableSelectionChangedFunc) } func hTableDoneFunc(key tcell.Key) { switch { case key == tcell.KeyEscape: app.Stop() case key == tcell.KeyTab: pages.SwitchToPage("Saturation-Value page") app.SetFocus(svTable) } } func hTableSelectedFunc(row int, column int) { hue = column * 2 // Switch to saturation-value page with the correct setup svTable.Clear() pages.SwitchToPage("Saturation-Value page") app.SetFocus(svTable) svTable.Select(0, 100) cursor := color.HSVtoRGB(color.HSV{H: (hue + 180) % 360, S: 100, V: 100}) c := tcell.NewRGBColor(int32(cursor.R), int32(cursor.G), int32(cursor.B)) svTable.SetSelectedStyle(c, c, tcell.AttrNone) drawSVTable() darkHSV := color.HSV{H: hue, S: 100, V: 99} lightHSV := color.HSV{H: hue, S: 100, V: 100} setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText) } func hTableSelectionChangedFunc(row int, column int) { darkHSV := color.HSV{H: column * 2, S: 100, V: 100} lightHSV := color.HSV{H: column*2 + 1, S: 100, V: 100} setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText) } // svTable setup --------------------------------------------------------- func svTableSetup() { drawSVTable() // 16842751 is cyan which makes the cursor stand out on red table svTable.SetSelectedStyle(16842751, 16842751, tcell.AttrNone) svTable.SetSelectable(true, true) svTable.SetCellPadding(0, 0) svTable.Select(0, 100) svTable.SetDoneFunc(svTableDoneFunc) svTable.SetSelectedFunc(svTableSelectedFunc) svTable.SetSelectionChangedFunc(svTableSelectionChangedFunc) } func svTableDoneFunc(key tcell.Key) { switch { case key == tcell.KeyEscape: app.Stop() case key == tcell.KeyTab: pages.SwitchToPage("Hue page") app.SetFocus(hFocus) } } func svTableSelectedFunc(row int, column int) { hsv := color.HSV{H: hue, S: column, V: 100 - row*2} rgb := color.HSVtoRGB(hsv) hsl := color.HSVtoHSL(hsv) cmyk := color.HSVtoCMYK(hsv) hex := color.HSVtoHex(hsv) decimal := color.HSVtoDecimal(hsv) ansi := color.HSVtoAnsi(hsv) altHsv := color.HSV{H: hue, S: column, V: 99 - row*2} name := getColorName(hsv, altHsv) returnColor = ColorValues{rgb, hsv, hsl, cmyk, hex, decimal, ansi, name} app.Stop() } func svTableSelectionChangedFunc(row int, column int) { // Set the dark saturation-value block to the correct color and the // saturation-value text to contain the right values var darkHSV color.HSV if row*2+1 > 100 { darkHSV = color.HSV{H: hue, S: column, V: 0} } else { darkHSV = color.HSV{H: hue, S: column, V: 100 - (row*2 + 1)} } lightHSV := color.HSV{H: hue, S: column, V: 100 - (row * 2)} setColorValues(darkHSV, darkSVBlock, darkSVText, lightHSV, lightSVBlock, lightSVText) } // Helper functions --------------------------------------------------- func drawSVTable() { // Draw the table with the correct hue for s := 0; s <= 100; s++ { for v := 0; v < 50; v++ { bg := color.HSVtoRGB(color.HSV{H: hue, S: s, V: 100 - v*2}) fg := color.HSVtoRGB(color.HSV{H: hue, S: s, V: 100 - (v*2 + 1)}) bc := tcell.NewRGBColor(int32(bg.R), int32(bg.G), int32(bg.B)) c := tcell.NewRGBColor(int32(fg.R), int32(fg.G), int32(fg.B)) cell := cview.NewTableCell("▄") cell.SetBackgroundColor(bc) cell.SetTextColor(c) svTable.SetCell(v, s, cell) } } for i := 0; i <= 100; i++ { cell := cview.NewTableCell(" ") cell.SetBackgroundColor(0) svTable.SetCell(50, i, cell) } } func setColorValues(darkHSV color.HSV, darkBlock *cview.TextView, darkText *cview.TextView, lightHSV color.HSV, lightBlock *cview.TextView, lightText *cview.TextView) { if darkHSV.S > 100 { darkHSV.S = 100 } else if darkHSV.S < 0 { darkHSV.S = 0 } if darkHSV.V > 100 { darkHSV.V = 100 } else if darkHSV.V < 0 { darkHSV.V = 0 } if lightHSV.S > 100 { lightHSV.S = 100 } else if lightHSV.S < 0 { lightHSV.S = 0 } if lightHSV.V > 100 { lightHSV.V = 100 } else if lightHSV.V < 0 { lightHSV.V = 0 } // Fill in the color blocks with the color info darkRGB := color.HSVtoRGB(darkHSV) darkHSL := color.HSVtoHSL(darkHSV) darkCMYK := color.HSVtoCMYK(darkHSV) darkHex := color.HSVtoHex(darkHSV) darkDecimal := color.HSVtoDecimal(darkHSV) darkAnsi := color.HSVtoAnsi(darkHSV) lightRGB := color.HSVtoRGB(lightHSV) lightHSL := color.HSVtoHSL(lightHSV) lightCMYK := color.HSVtoCMYK(lightHSV) lightHex := color.HSVtoHex(lightHSV) lightDecimal := color.HSVtoDecimal(lightHSV) lightAnsi := color.HSVtoAnsi(lightHSV) if !smallWidth && !smallHeight { dc := tcell.NewRGBColor(int32(darkRGB.R), int32(darkRGB.G), int32(darkRGB.B)) darkBlock.SetTextColor(dc) dText := fmt.Sprintf(colorTextWide, darkRGB.R, darkRGB.G, darkRGB.B, darkHSV.H, darkHSV.S, darkHSV.V, darkHSL.H, darkHSL.S, darkHSL.L, darkCMYK.C, darkCMYK.M, darkCMYK.Y, darkCMYK.K, darkHex, darkDecimal, darkAnsi) darkText.SetText(dText) lc := tcell.NewRGBColor(int32(lightRGB.R), int32(lightRGB.G), int32(lightRGB.B)) lightBlock.SetTextColor(lc) lText := fmt.Sprintf(colorTextWide, lightRGB.R, lightRGB.G, lightRGB.B, lightHSV.H, lightHSV.S, lightHSV.V, lightHSL.H, lightHSL.S, lightHSL.L, lightCMYK.C, lightCMYK.M, lightCMYK.Y, lightCMYK.K, lightHex, lightDecimal, lightAnsi) lightText.SetText(lText) } else { dc := tcell.NewRGBColor(int32(darkRGB.R), int32(darkRGB.G), int32(darkRGB.B)) darkBlock.SetTextColor(dc) dText := fmt.Sprintf(colorTextSmall, darkRGB.R, darkRGB.G, darkRGB.B, darkHSV.H, darkHSV.S, darkHSV.V, darkHSL.H, darkHSL.S, darkHSL.L, darkCMYK.C, darkCMYK.M, darkCMYK.Y, darkCMYK.K, darkHex, darkDecimal, darkAnsi) darkText.SetText(dText) } } func getColorName(hsv color.HSV, altHSV color.HSV) string { // If one of the preset colors is equal to the selected hsv, return the name var h color.HSV for i := 0; i < len(colorInfo); i++ { for _, c := range colorInfo[i].colors { h = color.HextoHSV(color.Hex(c.VALUE)) if h == hsv || h == altHSV { return c.NAME } } } return "custom color" } // Get the location of a searched color func getColorLocations(name string) [][]int { var locations [][]int for i := 0; i < len(colorInfo); i++ { for x := 0; x < int(math.Ceil(float64(colorInfo[i].length/9)))+1; x++ { for y := 0; y < 9; y++ { if strings.Contains(strings.ToLower(colorInfo[i].colors[x*9+y].NAME), name) { var location = []int{i, x, y} locations = append(locations, location) } } } } return locations } func getPath() (string, error) { usr, err := user.Current() testErr(err) homeDir := usr.HomeDir paths := [...]string{"./colors.json", homeDir + "/.config/cpick/colors.json", homeDir + "/.cpick/colors.json"} for i := 0; i < 3; i++ { if _, err := os.Stat(paths[i]); err == nil { // Path exists return paths[i], nil } else if os.IsNotExist(err) { // Path does not exist continue } } // Else: an error occurred return "", err } func getCustomColors(path string) jsonData { var data jsonData if path != "" { raw, err := ioutil.ReadFile(path) if testingMode { err = nil } testErr(err) if testingMode { raw = []byte(presetData) } err = json.Unmarshal([]byte(raw), &data) testErr(err) } else { err := json.Unmarshal([]byte(presetData), &data) testErr(err) } return data } func showHelp() { if searchFlex.HasFocus() { return } if hTable.HasFocus() { helpFocus = hTable hFlex.AddItem(helpFlex, 100, 1, false) } else if colorPages.HasFocus() { helpFocus = colorPages hFlex.AddItem(helpFlex, 100, 1, false) } else if svTable.HasFocus() { helpFocus = svTable svFlex.SetDirection(cview.FlexRow) svFlex.AddItem(helpFlex, 100, 1, false) } app.SetFocus(helpModal) } func showSearch() { pages.SwitchToPage("Search page") app.SetFocus(searchInput) } func testErr(err error) { if err != nil { log.Fatal(err) app.Stop() } } // Start function starts the cpick application. // Testing (bool) is used to test all of the functions to make sure they // can run properly without a need for user input (testing = true). func Start(testing bool) (ColorValues, error) { if testing { // If being run in testing mode, run the tester function testingMode = true tester() } else if !testingMode { // Find the width and height of the application app.Init() width, height := app.GetScreenSize() smallWidth = width < BREAKPOINT_WIDTH smallHeight = height < BREAKPOINT_HEIGHT } app.SetInputCapture(inputCaptureHandler) pages.AddPage("Hue page", hFlex, true, true) pages.AddPage("Saturation-Value page", svFlex, true, false) pages.AddPage("Search page", searchFlex, true, false) hTableSetup() svTableSetup() colorPageSetup() helpPageSetup() searchInputSetup() hScreenSetup() svScreenSetup() if !testingMode { app.SetRoot(pages, true) if err := app.Run(); err != nil { log.Fatal(err) panic(err) } } return returnColor, nil }
package cview import ( "bytes" "fmt" "io" "strconv" "strings" ) // The states of the ANSI escape code parser. const ( ansiText = iota ansiEscape ansiSubstring ansiControlSequence ) // ansi is a io.Writer which translates ANSI escape codes into cview color // tags. type ansi struct { io.Writer // Reusable buffers. buffer *bytes.Buffer // The entire output text of one Write(). csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings. attributes string // The buffer's current text attributes (a tview attribute string). // The current state of the parser. One of the ansi constants. state int } // ANSIWriter returns an io.Writer which translates any ANSI escape codes // written to it into cview color tags. Other escape codes don't have an effect // and are simply removed. The translated text is written to the provided // writer. func ANSIWriter(writer io.Writer) io.Writer { return &ansi{ Writer: writer, buffer: new(bytes.Buffer), csiParameter: new(bytes.Buffer), csiIntermediate: new(bytes.Buffer), state: ansiText, } } // Write parses the given text as a string of runes, translates ANSI escape // codes to color tags and writes them to the output writer. func (a *ansi) Write(text []byte) (int, error) { defer func() { a.buffer.Reset() }() for _, r := range string(text) { switch a.state { // We just entered an escape sequence. case ansiEscape: switch r { case '[': // Control Sequence Introducer. a.csiParameter.Reset() a.csiIntermediate.Reset() a.state = ansiControlSequence case 'c': // Reset. fmt.Fprint(a.buffer, "[-:-:-]") a.state = ansiText case 'P', ']', 'X', '^', '_': // Substrings and commands. a.state = ansiSubstring default: // Ignore. a.state = ansiText } // CSI Sequences. case ansiControlSequence: switch { case r >= 0x30 && r <= 0x3f: // Parameter bytes. if _, err := a.csiParameter.WriteRune(r); err != nil { return 0, err } case r >= 0x20 && r <= 0x2f: // Intermediate bytes. if _, err := a.csiIntermediate.WriteRune(r); err != nil { return 0, err } case r >= 0x40 && r <= 0x7e: // Final byte. switch r { case 'E': // Next line. count, _ := strconv.Atoi(a.csiParameter.String()) if count == 0 { count = 1 } fmt.Fprint(a.buffer, strings.Repeat("\n", count)) case 'm': // Select Graphic Rendition. var background, foreground string params := a.csiParameter.String() fields := strings.Split(params, ";") if len(params) == 0 || len(fields) == 1 && fields[0] == "0" { // Reset. a.attributes = "" if _, err := a.buffer.WriteString("[-:-:-]"); err != nil { return 0, err } break } lookupColor := func(colorNumber int) string { if colorNumber < 0 || colorNumber > 15 { return "black" } return []string{ "black", "maroon", "green", "olive", "navy", "purple", "teal", "silver", "gray", "red", "lime", "yellow", "blue", "fuchsia", "aqua", "white", }[colorNumber] } FieldLoop: for index, field := range fields { switch field { case "1", "01": if strings.IndexRune(a.attributes, 'b') < 0 { a.attributes += "b" } case "2", "02": if strings.IndexRune(a.attributes, 'd') < 0 { a.attributes += "d" } case "4", "04": if strings.IndexRune(a.attributes, 'u') < 0 { a.attributes += "u" } case "5", "05": if strings.IndexRune(a.attributes, 'l') < 0 { a.attributes += "l" } case "22": if i := strings.IndexRune(a.attributes, 'b'); i >= 0 { a.attributes = a.attributes[:i] + a.attributes[i+1:] } if i := strings.IndexRune(a.attributes, 'd'); i >= 0 { a.attributes = a.attributes[:i] + a.attributes[i+1:] } case "24": if i := strings.IndexRune(a.attributes, 'u'); i >= 0 { a.attributes = a.attributes[:i] + a.attributes[i+1:] } case "25": if i := strings.IndexRune(a.attributes, 'l'); i >= 0 { a.attributes = a.attributes[:i] + a.attributes[i+1:] } case "30", "31", "32", "33", "34", "35", "36", "37": colorNumber, _ := strconv.Atoi(field) foreground = lookupColor(colorNumber - 30) case "39": foreground = "-" case "40", "41", "42", "43", "44", "45", "46", "47": colorNumber, _ := strconv.Atoi(field) background = lookupColor(colorNumber - 40) case "49": background = "-" case "90", "91", "92", "93", "94", "95", "96", "97": colorNumber, _ := strconv.Atoi(field) foreground = lookupColor(colorNumber - 82) case "100", "101", "102", "103", "104", "105", "106", "107": colorNumber, _ := strconv.Atoi(field) background = lookupColor(colorNumber - 92) case "38", "48": var color string if len(fields) > index+1 { if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors. colorNumber, _ := strconv.Atoi(fields[index+2]) if colorNumber <= 15 { color = lookupColor(colorNumber) } else if colorNumber <= 231 { red := (colorNumber - 16) / 36 green := ((colorNumber - 16) / 6) % 6 blue := (colorNumber - 16) % 6 color = fmt.Sprintf("#%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5) } else if colorNumber <= 255 { grey := 255 * (colorNumber - 232) / 23 color = fmt.Sprintf("#%02x%02x%02x", grey, grey, grey) } } else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors. red, _ := strconv.Atoi(fields[index+2]) green, _ := strconv.Atoi(fields[index+3]) blue, _ := strconv.Atoi(fields[index+4]) color = fmt.Sprintf("#%02x%02x%02x", red, green, blue) } } if len(color) > 0 { if field == "38" { foreground = color } else { background = color } } break FieldLoop } } var colon string if len(a.attributes) > 0 { colon = ":" } if len(foreground) > 0 || len(background) > 0 || len(a.attributes) > 0 { fmt.Fprintf(a.buffer, "[%s:%s%s%s]", foreground, background, colon, a.attributes) } } a.state = ansiText default: // Undefined byte. a.state = ansiText // Abort CSI. } // We just entered a substring/command sequence. case ansiSubstring: if r == 27 { // Most likely the end of the substring. a.state = ansiEscape } // Ignore all other characters. // "ansiText" and all others. default: if r == 27 { // This is the start of an escape sequence. a.state = ansiEscape } else { // Just a regular rune. Send to buffer. if _, err := a.buffer.WriteRune(r); err != nil { return 0, err } } } } // Write buffer to target writer. n, err := a.buffer.WriteTo(a.Writer) if err != nil { return int(n), err } return len(text), nil } // TranslateANSI replaces ANSI escape sequences found in the provided string // with cview's color tags and returns the resulting string. func TranslateANSI(text string) string { var buffer bytes.Buffer writer := ANSIWriter(&buffer) writer.Write([]byte(text)) return buffer.String() }
package cview import ( "fmt" "sync" "time" "github.com/gdamore/tcell/v2" ) const ( // The size of the event/update/redraw channels. queueSize = 100 // The minimum duration between resize event callbacks. resizeEventThrottle = 50 * time.Millisecond ) // Application represents the top node of an application. // // It is not strictly required to use this class as none of the other classes // depend on it. However, it provides useful tools to set up an application and // plays nicely with all widgets. // // The following command displays a primitive p on the screen until Ctrl-C is // pressed: // // if err := cview.NewApplication().SetRoot(p, true).Run(); err != nil { // panic(err) // } type Application struct { // The application's screen. Apart from Run(), this variable should never be // set directly. Always use the screenReplacement channel after calling // Fini(), to set a new screen (or nil to stop the application). screen tcell.Screen // The size of the application's screen. width, height int // The primitive which currently has the keyboard focus. focus Primitive // The root primitive to be seen on the screen. root Primitive // Whether or not the application resizes the root primitive. rootFullscreen bool // Whether or not to enable bracketed paste mode. enableBracketedPaste bool // Whether or not to enable mouse events. enableMouse bool // An optional capture function which receives a key event and returns the // event to be forwarded to the default input handler (nil if nothing should // be forwarded). inputCapture func(event *tcell.EventKey) *tcell.EventKey // Time a resize event was last processed. lastResize time.Time // Timer limiting how quickly resize events are processed. throttleResize *time.Timer // An optional callback function which is invoked when the application's // window is initialized, and when the application's window size changes. // After invoking this callback the screen is cleared and the application // is drawn. afterResize func(width int, height int) // An optional callback function which is invoked before the application's // focus changes. beforeFocus func(p Primitive) bool // An optional callback function which is invoked after the application's // focus changes. afterFocus func(p Primitive) // An optional callback function which is invoked just before the root // primitive is drawn. beforeDraw func(screen tcell.Screen) bool // An optional callback function which is invoked after the root primitive // was drawn. afterDraw func(screen tcell.Screen) // Used to send screen events from separate goroutine to main event loop events chan tcell.Event // Functions queued from goroutines, used to serialize updates to primitives. updates chan func() // An object that the screen variable will be set to after Fini() was called. // Use this channel to set a new screen object for the application // (screen.Init() and draw() will be called implicitly). A value of nil will // stop the application. screenReplacement chan tcell.Screen // An optional capture function which receives a mouse event and returns the // event to be forwarded to the default mouse handler (nil if nothing should // be forwarded). mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) // doubleClickInterval specifies the maximum time between clicks to register a // double click rather than a single click. doubleClickInterval time.Duration mouseCapturingPrimitive Primitive // A Primitive returned by a MouseHandler which will capture future mouse events. lastMouseX, lastMouseY int // The last position of the mouse. mouseDownX, mouseDownY int // The position of the mouse when its button was last pressed. lastMouseClick time.Time // The time when a mouse button was last clicked. lastMouseButtons tcell.ButtonMask // The last mouse button state. sync.RWMutex } // NewApplication creates and returns a new application. func NewApplication() *Application { return &Application{ enableBracketedPaste: true, events: make(chan tcell.Event, queueSize), updates: make(chan func(), queueSize), screenReplacement: make(chan tcell.Screen, 1), } } // SetInputCapture sets a function which captures all key events before they are // forwarded to the key event handler of the primitive which currently has // focus. This function can then choose to forward that key event (or a // different one) by returning it or stop the key event processing by returning // nil. // // Note that this also affects the default event handling of the application // itself: Such a handler can intercept the Ctrl-C event which closes the // application. func (a *Application) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) { a.Lock() defer a.Unlock() a.inputCapture = capture } // GetInputCapture returns the function installed with SetInputCapture() or nil // if no such function has been installed. func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey { a.RLock() defer a.RUnlock() return a.inputCapture } // SetMouseCapture sets a function which captures mouse events (consisting of // the original tcell mouse event and the semantic mouse action) before they are // forwarded to the appropriate mouse event handler. This function can then // choose to forward that event (or a different one) by returning it or stop // the event processing by returning a nil mouse event. func (a *Application) SetMouseCapture(capture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) { a.mouseCapture = capture } // GetMouseCapture returns the function installed with SetMouseCapture() or nil // if no such function has been installed. func (a *Application) GetMouseCapture() func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) { return a.mouseCapture } // SetDoubleClickInterval sets the maximum time between clicks to register a // double click rather than a single click. A standard duration is provided as // StandardDoubleClick. No interval is set by default, disabling double clicks. func (a *Application) SetDoubleClickInterval(interval time.Duration) { a.doubleClickInterval = interval } // SetScreen allows you to provide your own tcell.Screen object. For most // applications, this is not needed and you should be familiar with // tcell.Screen when using this function. // // This function is typically called before the first call to Run(). Init() need // not be called on the screen. func (a *Application) SetScreen(screen tcell.Screen) { if screen == nil { return // Invalid input. Do nothing. } a.Lock() if a.screen == nil { // Run() has not been called yet. a.screen = screen a.Unlock() return } // Run() is already in progress. Exchange screen. oldScreen := a.screen a.Unlock() oldScreen.Fini() a.screenReplacement <- screen } // GetScreen returns the current tcell.Screen of the application. Lock the // application when manipulating the screen to prevent race conditions. This // value is only available after calling Init or Run. func (a *Application) GetScreen() tcell.Screen { a.RLock() defer a.RUnlock() return a.screen } // GetScreenSize returns the size of the application's screen. These values are // only available after calling Init or Run. func (a *Application) GetScreenSize() (width, height int) { a.RLock() defer a.RUnlock() return a.width, a.height } // Init initializes the application screen. Calling Init before running is not // required. Its primary use is to populate screen dimensions before running an // application. func (a *Application) Init() error { a.Lock() defer a.Unlock() return a.init() } func (a *Application) init() error { if a.screen != nil { return nil } var err error a.screen, err = tcell.NewScreen() if err != nil { a.Unlock() return err } if err = a.screen.Init(); err != nil { a.Unlock() return err } a.width, a.height = a.screen.Size() if a.enableBracketedPaste { a.screen.EnablePaste() } if a.enableMouse { a.screen.EnableMouse() } return nil } // EnableBracketedPaste enables bracketed paste mode, which is enabled by default. func (a *Application) EnableBracketedPaste(enable bool) { a.Lock() defer a.Unlock() if enable != a.enableBracketedPaste && a.screen != nil { if enable { a.screen.EnablePaste() } else { a.screen.DisablePaste() } } a.enableBracketedPaste = enable } // EnableMouse enables mouse events. func (a *Application) EnableMouse(enable bool) { a.Lock() defer a.Unlock() if enable != a.enableMouse && a.screen != nil { if enable { a.screen.EnableMouse() } else { a.screen.DisableMouse() } } a.enableMouse = enable } // Run starts the application and thus the event loop. This function returns // when Stop() was called. func (a *Application) Run() error { a.Lock() // Initialize screen err := a.init() if err != nil { return err } // We catch panics to clean up because they mess up the terminal. defer func() { if p := recover(); p != nil { if a.screen != nil { a.screen.Fini() } panic(p) } }() // Draw the screen for the first time. a.Unlock() a.draw() // Separate loop to wait for screen events. var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for { a.RLock() screen := a.screen a.RUnlock() if screen == nil { // We have no screen. Let's stop. a.QueueEvent(nil) break } // Wait for next event and queue it. event := screen.PollEvent() if event != nil { // Regular event. Queue. a.QueueEvent(event) continue } // A screen was finalized (event is nil). Wait for a new screen. screen = <-a.screenReplacement if screen == nil { // No new screen. We're done. a.QueueEvent(nil) return } // We have a new screen. Keep going. a.Lock() a.screen = screen a.Unlock() // Initialize and draw this screen. if err := screen.Init(); err != nil { panic(err) } if a.enableBracketedPaste { screen.EnablePaste() } if a.enableMouse { screen.EnableMouse() } a.draw() } }() handle := func(event interface{}) { a.RLock() p := a.focus inputCapture := a.inputCapture screen := a.screen a.RUnlock() switch event := event.(type) { case *tcell.EventKey: // Intercept keys. if inputCapture != nil { event = inputCapture(event) if event == nil { a.draw() return // Don't forward event. } } // Ctrl-C closes the application. if event.Key() == tcell.KeyCtrlC { a.Stop() return } // Pass other key events to the currently focused primitive. if p != nil { if handler := p.InputHandler(); handler != nil { handler(event, func(p Primitive) { a.SetFocus(p) }) a.draw() } } case *tcell.EventResize: // Throttle resize events. if time.Since(a.lastResize) < resizeEventThrottle { // Stop timer if a.throttleResize != nil && !a.throttleResize.Stop() { select { case <-a.throttleResize.C: default: } } event := event // Capture // Start timer a.throttleResize = time.AfterFunc(resizeEventThrottle, func() { a.events <- event }) return } a.lastResize = time.Now() if screen == nil { return } screen.Clear() a.width, a.height = event.Size() // Call afterResize handler if there is one. if a.afterResize != nil { a.afterResize(a.width, a.height) } a.draw() case *tcell.EventMouse: consumed, isMouseDownAction := a.fireMouseActions(event) if consumed { a.draw() } a.lastMouseButtons = event.Buttons() if isMouseDownAction { a.mouseDownX, a.mouseDownY = event.Position() } } } // Start event loop. EventLoop: for { // Handle events before executing updates select { case event := <-a.events: if event == nil { break EventLoop } handle(event) continue default: } select { case event := <-a.events: if event == nil { break EventLoop } handle(event) case update := <-a.updates: update() } } // Wait for the event loop to finish. wg.Wait() a.screen = nil return nil } // fireMouseActions analyzes the provided mouse event, derives mouse actions // from it and then forwards them to the corresponding primitives. func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMouseDownAction bool) { // We want to relay follow-up events to the same target primitive. var targetPrimitive Primitive // Helper function to fire a mouse action. fire := func(action MouseAction) { switch action { case MouseLeftDown, MouseMiddleDown, MouseRightDown: isMouseDownAction = true } // Intercept event. if a.mouseCapture != nil { event, action = a.mouseCapture(event, action) if event == nil { consumed = true return // Don't forward event. } } // Determine the target primitive. var primitive, capturingPrimitive Primitive if a.mouseCapturingPrimitive != nil { primitive = a.mouseCapturingPrimitive targetPrimitive = a.mouseCapturingPrimitive } else if targetPrimitive != nil { primitive = targetPrimitive } else { primitive = a.root } if primitive != nil { if handler := primitive.MouseHandler(); handler != nil { var wasConsumed bool wasConsumed, capturingPrimitive = handler(action, event, func(p Primitive) { a.SetFocus(p) }) if wasConsumed { consumed = true } } } a.mouseCapturingPrimitive = capturingPrimitive } x, y := event.Position() buttons := event.Buttons() clickMoved := x != a.mouseDownX || y != a.mouseDownY buttonChanges := buttons ^ a.lastMouseButtons if x != a.lastMouseX || y != a.lastMouseY { fire(MouseMove) a.lastMouseX = x a.lastMouseY = y } for _, buttonEvent := range []struct { button tcell.ButtonMask down, up, click, dclick MouseAction }{ {tcell.ButtonPrimary, MouseLeftDown, MouseLeftUp, MouseLeftClick, MouseLeftDoubleClick}, {tcell.ButtonMiddle, MouseMiddleDown, MouseMiddleUp, MouseMiddleClick, MouseMiddleDoubleClick}, {tcell.ButtonSecondary, MouseRightDown, MouseRightUp, MouseRightClick, MouseRightDoubleClick}, } { if buttonChanges&buttonEvent.button != 0 { if buttons&buttonEvent.button != 0 { fire(buttonEvent.down) } else { fire(buttonEvent.up) if !clickMoved { if a.doubleClickInterval == 0 || a.lastMouseClick.Add(a.doubleClickInterval).Before(time.Now()) { fire(buttonEvent.click) a.lastMouseClick = time.Now() } else { fire(buttonEvent.dclick) a.lastMouseClick = time.Time{} // reset } } } } } for _, wheelEvent := range []struct { button tcell.ButtonMask action MouseAction }{ {tcell.WheelUp, MouseScrollUp}, {tcell.WheelDown, MouseScrollDown}, {tcell.WheelLeft, MouseScrollLeft}, {tcell.WheelRight, MouseScrollRight}} { if buttons&wheelEvent.button != 0 { fire(wheelEvent.action) } } return consumed, isMouseDownAction } // Stop stops the application, causing Run() to return. func (a *Application) Stop() { a.Lock() defer a.Unlock() screen := a.screen if screen == nil { return } a.screen = nil screen.Fini() a.screenReplacement <- nil } // Suspend temporarily suspends the application by exiting terminal UI mode and // invoking the provided function "f". When "f" returns, terminal UI mode is // entered again and the application resumes. // // A return value of true indicates that the application was suspended and "f" // was called. If false is returned, the application was already suspended, // terminal UI mode was not exited, and "f" was not called. func (a *Application) Suspend(f func()) bool { a.RLock() screen := a.screen a.RUnlock() if screen == nil { return false // Screen has not yet been initialized. } // Enter suspended mode. screen.Fini() // Wait for "f" to return. f() // Make a new screen. var err error screen, err = tcell.NewScreen() if err != nil { panic(err) } a.screenReplacement <- screen // Continue application loop. return true } // Draw refreshes the screen (during the next update cycle). It calls the Draw() // function of the application's root primitive and then syncs the screen // buffer. func (a *Application) Draw() { a.QueueUpdate(func() { a.draw() }) } // draw actually does what Draw() promises to do. func (a *Application) draw() { a.Lock() screen := a.screen root := a.root fullscreen := a.rootFullscreen before := a.beforeDraw after := a.afterDraw // Maybe we're not ready yet or not anymore. if screen == nil || root == nil { a.Unlock() return } // Resize if requested. if fullscreen { root.SetRect(0, 0, a.width, a.height) } // Call before handler if there is one. if before != nil { a.Unlock() if before(screen) { screen.Show() return } } else { a.Unlock() } // Draw all primitives. root.Draw(screen) // Call after handler if there is one. if after != nil { after(screen) } // Sync screen. screen.Show() } // SetBeforeDrawFunc installs a callback function which is invoked just before // the root primitive is drawn during screen updates. If the function returns // true, drawing will not continue, i.e. the root primitive will not be drawn // (and an after-draw-handler will not be called). // // Note that the screen is not cleared by the application. To clear the screen, // you may call screen.Clear(). // // Provide nil to uninstall the callback function. func (a *Application) SetBeforeDrawFunc(handler func(screen tcell.Screen) bool) { a.Lock() defer a.Unlock() a.beforeDraw = handler } // GetBeforeDrawFunc returns the callback function installed with // SetBeforeDrawFunc() or nil if none has been installed. func (a *Application) GetBeforeDrawFunc() func(screen tcell.Screen) bool { a.RLock() defer a.RUnlock() return a.beforeDraw } // SetAfterDrawFunc installs a callback function which is invoked after the root // primitive was drawn during screen updates. // // Provide nil to uninstall the callback function. func (a *Application) SetAfterDrawFunc(handler func(screen tcell.Screen)) { a.Lock() defer a.Unlock() a.afterDraw = handler } // GetAfterDrawFunc returns the callback function installed with // SetAfterDrawFunc() or nil if none has been installed. func (a *Application) GetAfterDrawFunc() func(screen tcell.Screen) { a.RLock() defer a.RUnlock() return a.afterDraw } // SetRoot sets the root primitive for this application. If "fullscreen" is set // to true, the root primitive's position will be changed to fill the screen. // // This function must be called at least once or nothing will be displayed when // the application starts. // // It also calls SetFocus() on the primitive. func (a *Application) SetRoot(root Primitive, fullscreen bool) { a.Lock() a.root = root a.rootFullscreen = fullscreen if a.screen != nil { a.screen.Clear() } a.Unlock() a.SetFocus(root) } // ResizeToFullScreen resizes the given primitive such that it fills the entire // screen. func (a *Application) ResizeToFullScreen(p Primitive) { a.RLock() width, height := a.width, a.height a.RUnlock() p.SetRect(0, 0, width, height) } // SetAfterResizeFunc installs a callback function which is invoked when the // application's window is initialized, and when the application's window size // changes. After invoking this callback the screen is cleared and the // application is drawn. // // Provide nil to uninstall the callback function. func (a *Application) SetAfterResizeFunc(handler func(width int, height int)) { a.Lock() defer a.Unlock() a.afterResize = handler } // GetAfterResizeFunc returns the callback function installed with // SetAfterResizeFunc() or nil if none has been installed. func (a *Application) GetAfterResizeFunc() func(width int, height int) { a.RLock() defer a.RUnlock() return a.afterResize } // SetFocus sets the focus on a new primitive. All key events will be redirected // to that primitive. Callers must ensure that the primitive will handle key // events. // // Blur() will be called on the previously focused primitive. Focus() will be // called on the new primitive. func (a *Application) SetFocus(p Primitive) { a.Lock() if a.beforeFocus != nil { a.Unlock() ok := a.beforeFocus(p) if !ok { return } a.Lock() } if a.focus != nil { a.focus.Blur() } a.focus = p if a.screen != nil { a.screen.HideCursor() } if a.afterFocus != nil { a.Unlock() a.afterFocus(p) } else { a.Unlock() } if p != nil { p.Focus(func(p Primitive) { a.SetFocus(p) }) } } // GetFocus returns the primitive which has the current focus. If none has it, // nil is returned. func (a *Application) GetFocus() Primitive { a.RLock() defer a.RUnlock() return a.focus } // SetBeforeFocusFunc installs a callback function which is invoked before the // application's focus changes. Return false to maintain the current focus. // // Provide nil to uninstall the callback function. func (a *Application) SetBeforeFocusFunc(handler func(p Primitive) bool) { a.Lock() defer a.Unlock() a.beforeFocus = handler } // SetAfterFocusFunc installs a callback function which is invoked after the // application's focus changes. // // Provide nil to uninstall the callback function. func (a *Application) SetAfterFocusFunc(handler func(p Primitive)) { a.Lock() defer a.Unlock() a.afterFocus = handler } // QueueUpdate queues a function to be executed as part of the event loop. // // Note that Draw() is not implicitly called after the execution of f as that // may not be desirable. You can call Draw() from f if the screen should be // refreshed after each update. Alternatively, use QueueUpdateDraw() to follow // up with an immediate refresh of the screen. func (a *Application) QueueUpdate(f func()) { a.updates <- f } // QueueUpdateDraw works like QueueUpdate() except it refreshes the screen // immediately after executing f. func (a *Application) QueueUpdateDraw(f func()) { a.QueueUpdate(func() { f() a.draw() }) } // QueueEvent sends an event to the Application event loop. // // It is not recommended for event to be nil. func (a *Application) QueueEvent(event tcell.Event) { a.events <- event } // RingBell sends a bell code to the terminal. func (a *Application) RingBell() { a.QueueUpdate(func() { fmt.Print(string(byte(7))) }) }
package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // Box is the base Primitive for all widgets. It has a background color and // optional surrounding elements such as a border and a title. It does not have // inner text. Widgets embed Box and draw their text over it. type Box struct { // The position of the rect. x, y, width, height int // Padding. paddingTop, paddingBottom, paddingLeft, paddingRight int // The inner rect reserved for the box's content. innerX, innerY, innerWidth, innerHeight int // Whether or not the box is visible. visible bool // The border color when the box has focus. borderColorFocused tcell.Color // The box's background color. backgroundColor tcell.Color // Whether or not the box's background is transparent. backgroundTransparent bool // Whether or not a border is drawn, reducing the box's space for content by // two in width and height. border bool // The color of the border. borderColor tcell.Color // The style attributes of the border. borderAttributes tcell.AttrMask // The title. Only visible if there is a border, too. title []byte // The color of the title. titleColor tcell.Color // The alignment of the title. titleAlign int // Provides a way to find out if this box has focus. We always go through // this interface because it may be overridden by implementing classes. focus Focusable // Whether or not this box has focus. hasFocus bool // Whether or not this box shows its focus. showFocus bool // An optional capture function which receives a key event and returns the // event to be forwarded to the primitive's default input handler (nil if // nothing should be forwarded). inputCapture func(event *tcell.EventKey) *tcell.EventKey // An optional function which is called before the box is drawn. draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) // An optional capture function which receives a mouse event and returns the // event to be forwarded to the primitive's default mouse event handler (at // least one nil if nothing should be forwarded). mouseCapture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) l sync.RWMutex } // NewBox returns a Box without a border. func NewBox() *Box { b := &Box{ width: 15, height: 10, visible: true, backgroundColor: Styles.PrimitiveBackgroundColor, borderColor: Styles.BorderColor, titleColor: Styles.TitleColor, borderColorFocused: ColorUnset, titleAlign: AlignCenter, showFocus: true, } b.focus = b b.updateInnerRect() return b } func (b *Box) updateInnerRect() { x, y, width, height := b.x, b.y, b.width, b.height // Subtract border space if b.border { x++ y++ width -= 2 height -= 2 } // Subtract padding x, y, width, height = x+b.paddingLeft, y+b.paddingTop, width-b.paddingLeft-b.paddingRight, height-b.paddingTop-b.paddingBottom if width < 0 { width = 0 } if height < 0 { height = 0 } b.innerX, b.innerY, b.innerWidth, b.innerHeight = x, y, width, height } // GetPadding returns the size of the padding around the box content. func (b *Box) GetPadding() (top, bottom, left, right int) { b.l.RLock() defer b.l.RUnlock() return b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight } // SetPadding sets the size of the padding around the box content. func (b *Box) SetPadding(top, bottom, left, right int) { b.l.Lock() defer b.l.Unlock() b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight = top, bottom, left, right b.updateInnerRect() } // GetRect returns the current position of the rectangle, x, y, width, and // height. func (b *Box) GetRect() (int, int, int, int) { b.l.RLock() defer b.l.RUnlock() return b.x, b.y, b.width, b.height } // GetInnerRect returns the position of the inner rectangle (x, y, width, // height), without the border and without any padding. Width and height values // will clamp to 0 and thus never be negative. func (b *Box) GetInnerRect() (int, int, int, int) { b.l.RLock() defer b.l.RUnlock() return b.innerX, b.innerY, b.innerWidth, b.innerHeight } // SetRect sets a new position of the primitive. Note that this has no effect // if this primitive is part of a layout (e.g. Flex, Grid) or if it was added // like this: // // application.SetRoot(b, true) func (b *Box) SetRect(x, y, width, height int) { b.l.Lock() defer b.l.Unlock() b.x, b.y, b.width, b.height = x, y, width, height b.updateInnerRect() } // SetVisible sets the flag indicating whether or not the box is visible. func (b *Box) SetVisible(v bool) { b.l.Lock() defer b.l.Unlock() b.visible = v } // GetVisible returns a value indicating whether or not the box is visible. func (b *Box) GetVisible() bool { b.l.RLock() defer b.l.RUnlock() return b.visible } // SetDrawFunc sets a callback function which is invoked after the box primitive // has been drawn. This allows you to add a more individual style to the box // (and all primitives which extend it). // // The function is provided with the box's dimensions (set via SetRect()). It // must return the box's inner dimensions (x, y, width, height) which will be // returned by GetInnerRect(), used by descendent primitives to draw their own // content. func (b *Box) SetDrawFunc(handler func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)) { b.l.Lock() defer b.l.Unlock() b.draw = handler } // GetDrawFunc returns the callback function which was installed with // SetDrawFunc() or nil if no such function has been installed. func (b *Box) GetDrawFunc() func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) { b.l.RLock() defer b.l.RUnlock() return b.draw } // WrapInputHandler wraps an input handler (see InputHandler()) with the // functionality to capture input (see SetInputCapture()) before passing it // on to the provided (default) input handler. // // This is only meant to be used by subclassing primitives. func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primitive))) func(*tcell.EventKey, func(p Primitive)) { return func(event *tcell.EventKey, setFocus func(p Primitive)) { if b.inputCapture != nil { event = b.inputCapture(event) } if event != nil && inputHandler != nil { inputHandler(event, setFocus) } } } // InputHandler returns nil. func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { b.l.RLock() defer b.l.RUnlock() return b.WrapInputHandler(nil) } // SetInputCapture installs a function which captures key events before they are // forwarded to the primitive's default key event handler. This function can // then choose to forward that key event (or a different one) to the default // handler by returning it. If nil is returned, the default handler will not // be called. // // Providing a nil handler will remove a previously existing handler. // // Note that this function will not have an effect on primitives composed of // other primitives, such as Form, Flex, or Grid. Key events are only captured // by the primitives that have focus (e.g. InputField) and only one primitive // can have focus at a time. Composing primitives such as Form pass the focus on // to their contained primitives and thus never receive any key events // themselves. Therefore, they cannot intercept key events. func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) { b.l.Lock() defer b.l.Unlock() b.inputCapture = capture } // GetInputCapture returns the function installed with SetInputCapture() or nil // if no such function has been installed. func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey { b.l.RLock() defer b.l.RUnlock() return b.inputCapture } // WrapMouseHandler wraps a mouse event handler (see MouseHandler()) with the // functionality to capture mouse events (see SetMouseCapture()) before passing // them on to the provided (default) event handler. // // This is only meant to be used by subclassing primitives. func (b *Box) WrapMouseHandler(mouseHandler func(MouseAction, *tcell.EventMouse, func(p Primitive)) (bool, Primitive)) func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if b.mouseCapture != nil { action, event = b.mouseCapture(action, event) } if event != nil && mouseHandler != nil { consumed, capture = mouseHandler(action, event, setFocus) } return } } // MouseHandler returns nil. func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if action == MouseLeftClick && b.InRect(event.Position()) { setFocus(b) consumed = true } return }) } // SetMouseCapture sets a function which captures mouse events (consisting of // the original tcell mouse event and the semantic mouse action) before they are // forwarded to the primitive's default mouse event handler. This function can // then choose to forward that event (or a different one) by returning it or // returning a nil mouse event, in which case the default handler will not be // called. // // Providing a nil handler will remove a previously existing handler. func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) { b.mouseCapture = capture } // InRect returns true if the given coordinate is within the bounds of the box's // rectangle. func (b *Box) InRect(x, y int) bool { rectX, rectY, width, height := b.GetRect() return x >= rectX && x < rectX+width && y >= rectY && y < rectY+height } // GetMouseCapture returns the function installed with SetMouseCapture() or nil // if no such function has been installed. func (b *Box) GetMouseCapture() func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) { return b.mouseCapture } // SetBackgroundColor sets the box's background color. func (b *Box) SetBackgroundColor(color tcell.Color) { b.l.Lock() defer b.l.Unlock() b.backgroundColor = color } // GetBackgroundColor returns the box's background color. func (b *Box) GetBackgroundColor() tcell.Color { b.l.RLock() defer b.l.RUnlock() return b.backgroundColor } // SetBackgroundTransparent sets the flag indicating whether or not the box's // background is transparent. The screen is not cleared before drawing the // application. Overlaying transparent widgets directly onto the screen may // result in artifacts. To resolve this, add a blank, non-transparent Box to // the bottom layer of the interface via Panels, or set a handler via // SetBeforeDrawFunc which clears the screen. func (b *Box) SetBackgroundTransparent(transparent bool) { b.l.Lock() defer b.l.Unlock() b.backgroundTransparent = transparent } // GetBorder returns a value indicating whether the box have a border // or not. func (b *Box) GetBorder() bool { b.l.RLock() defer b.l.RUnlock() return b.border } // SetBorder sets the flag indicating whether or not the box should have a // border. func (b *Box) SetBorder(show bool) { b.l.Lock() defer b.l.Unlock() b.border = show b.updateInnerRect() } // SetBorderColor sets the box's border color. func (b *Box) SetBorderColor(color tcell.Color) { b.l.Lock() defer b.l.Unlock() b.borderColor = color } // SetBorderColorFocused sets the box's border color when the box is focused. func (b *Box) SetBorderColorFocused(color tcell.Color) { b.l.Lock() defer b.l.Unlock() b.borderColorFocused = color } // SetBorderAttributes sets the border's style attributes. You can combine // different attributes using bitmask operations: // // box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold) func (b *Box) SetBorderAttributes(attr tcell.AttrMask) { b.l.Lock() defer b.l.Unlock() b.borderAttributes = attr } // SetTitle sets the box's title. func (b *Box) SetTitle(title string) { b.l.Lock() defer b.l.Unlock() b.title = []byte(title) } // GetTitle returns the box's current title. func (b *Box) GetTitle() string { b.l.RLock() defer b.l.RUnlock() return string(b.title) } // SetTitleColor sets the box's title color. func (b *Box) SetTitleColor(color tcell.Color) { b.l.Lock() defer b.l.Unlock() b.titleColor = color } // SetTitleAlign sets the alignment of the title, one of AlignLeft, AlignCenter, // or AlignRight. func (b *Box) SetTitleAlign(align int) { b.l.Lock() defer b.l.Unlock() b.titleAlign = align } // Draw draws this primitive onto the screen. func (b *Box) Draw(screen tcell.Screen) { b.l.Lock() defer b.l.Unlock() // Don't draw anything if the box is hidden if !b.visible { return } // Don't draw anything if there is no space. if b.width <= 0 || b.height <= 0 { return } def := tcell.StyleDefault // Fill background. background := def.Background(b.backgroundColor) if !b.backgroundTransparent { for y := b.y; y < b.y+b.height; y++ { for x := b.x; x < b.x+b.width; x++ { screen.SetContent(x, y, ' ', nil, background) } } } // Draw border. if b.border && b.width >= 2 && b.height >= 2 { border := SetAttributes(background.Foreground(b.borderColor), b.borderAttributes) var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune var hasFocus bool if b.focus == b { hasFocus = b.hasFocus } else { hasFocus = b.focus.HasFocus() } if hasFocus && b.borderColorFocused != ColorUnset { border = SetAttributes(background.Foreground(b.borderColorFocused), b.borderAttributes) } if hasFocus && b.showFocus { horizontal = Borders.HorizontalFocus vertical = Borders.VerticalFocus topLeft = Borders.TopLeftFocus topRight = Borders.TopRightFocus bottomLeft = Borders.BottomLeftFocus bottomRight = Borders.BottomRightFocus } else { horizontal = Borders.Horizontal vertical = Borders.Vertical topLeft = Borders.TopLeft topRight = Borders.TopRight bottomLeft = Borders.BottomLeft bottomRight = Borders.BottomRight } for x := b.x + 1; x < b.x+b.width-1; x++ { screen.SetContent(x, b.y, horizontal, nil, border) screen.SetContent(x, b.y+b.height-1, horizontal, nil, border) } for y := b.y + 1; y < b.y+b.height-1; y++ { screen.SetContent(b.x, y, vertical, nil, border) screen.SetContent(b.x+b.width-1, y, vertical, nil, border) } screen.SetContent(b.x, b.y, topLeft, nil, border) screen.SetContent(b.x+b.width-1, b.y, topRight, nil, border) screen.SetContent(b.x, b.y+b.height-1, bottomLeft, nil, border) screen.SetContent(b.x+b.width-1, b.y+b.height-1, bottomRight, nil, border) // Draw title. if len(b.title) > 0 && b.width >= 4 { printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor) if len(b.title)-printed > 0 && printed > 0 { _, _, style, _ := screen.GetContent(b.x+b.width-2, b.y) fg, _, _ := style.Decompose() Print(screen, []byte(string(SemigraphicsHorizontalEllipsis)), b.x+b.width-2, b.y, 1, AlignLeft, fg) } } } // Call custom draw function. if b.draw != nil { b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.draw(screen, b.x, b.y, b.width, b.height) } } // ShowFocus sets the flag indicating whether or not the borders of this // primitive should change thickness when focused. func (b *Box) ShowFocus(showFocus bool) { b.l.Lock() defer b.l.Unlock() b.showFocus = showFocus } // Focus is called when this primitive receives focus. func (b *Box) Focus(delegate func(p Primitive)) { b.l.Lock() defer b.l.Unlock() b.hasFocus = true } // Blur is called when this primitive loses focus. func (b *Box) Blur() { b.l.Lock() defer b.l.Unlock() b.hasFocus = false } // HasFocus returns whether or not this primitive has focus. func (b *Box) HasFocus() bool { b.l.RLock() defer b.l.RUnlock() return b.hasFocus } // GetFocusable returns the item's Focusable. func (b *Box) GetFocusable() Focusable { b.l.RLock() defer b.l.RUnlock() return b.focus } // GetBorderPadding returns the size of the padding around the box content. // // Deprecated: This function is provided for backwards compatibility. // Developers should use GetPadding instead. func (b *Box) GetBorderPadding() (top, bottom, left, right int) { return b.GetPadding() } // SetBorderPadding sets the size of the padding around the box content. // // Deprecated: This function is provided for backwards compatibility. // Developers should use SetPadding instead. func (b *Box) SetBorderPadding(top, bottom, left, right int) { b.SetPadding(top, bottom, left, right) }
package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // Button is labeled box that triggers an action when selected. type Button struct { *Box // The text to be displayed before the input area. label []byte // The label color. labelColor tcell.Color // The label color when the button is in focus. labelColorFocused tcell.Color // The background color when the button is in focus. backgroundColorFocused tcell.Color // An optional function which is called when the button was selected. selected func() // An optional function which is called when the user leaves the button. A // key is provided indicating which key was pressed to leave (tab or backtab). blur func(tcell.Key) sync.RWMutex } // NewButton returns a new input field. func NewButton(label string) *Button { box := NewBox() box.SetBackgroundColor(Styles.ContrastBackgroundColor) box.SetRect(0, 0, TaggedStringWidth(label)+4, 1) return &Button{ Box: box, label: []byte(label), labelColor: Styles.PrimaryTextColor, labelColorFocused: Styles.InverseTextColor, backgroundColorFocused: Styles.PrimaryTextColor, } } // SetLabel sets the button text. func (b *Button) SetLabel(label string) { b.Lock() defer b.Unlock() b.label = []byte(label) } // GetLabel returns the button text. func (b *Button) GetLabel() string { b.RLock() defer b.RUnlock() return string(b.label) } // SetLabelColor sets the color of the button text. func (b *Button) SetLabelColor(color tcell.Color) { b.Lock() defer b.Unlock() b.labelColor = color } // SetLabelColorFocused sets the color of the button text when the button is // in focus. func (b *Button) SetLabelColorFocused(color tcell.Color) { b.Lock() defer b.Unlock() b.labelColorFocused = color } // SetBackgroundColorFocused sets the background color of the button text when // the button is in focus. func (b *Button) SetBackgroundColorFocused(color tcell.Color) { b.Lock() defer b.Unlock() b.backgroundColorFocused = color } // SetSelectedFunc sets a handler which is called when the button was selected. func (b *Button) SetSelectedFunc(handler func()) { b.Lock() defer b.Unlock() b.selected = handler } // SetBlurFunc sets a handler which is called when the user leaves the button. // The callback function is provided with the key that was pressed, which is one // of the following: // // - KeyEscape: Leaving the button with no specific direction. // - KeyTab: Move to the next field. // - KeyBacktab: Move to the previous field. func (b *Button) SetBlurFunc(handler func(key tcell.Key)) { b.Lock() defer b.Unlock() b.blur = handler } // Draw draws this primitive onto the screen. func (b *Button) Draw(screen tcell.Screen) { if !b.GetVisible() { return } b.Lock() defer b.Unlock() // Draw the box. borderColor := b.borderColor backgroundColor := b.backgroundColor if b.focus.HasFocus() { b.backgroundColor = b.backgroundColorFocused b.borderColor = b.labelColorFocused defer func() { b.borderColor = borderColor }() } b.Unlock() b.Box.Draw(screen) b.Lock() b.backgroundColor = backgroundColor // Draw label. x, y, width, height := b.GetInnerRect() if width > 0 && height > 0 { y = y + height/2 labelColor := b.labelColor if b.focus.HasFocus() { labelColor = b.labelColorFocused } Print(screen, b.label, x, y, width, AlignCenter, labelColor) } } // InputHandler returns the handler for this primitive. func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { // Process key event. if HitShortcut(event, Keys.Select, Keys.Select2) { if b.selected != nil { b.selected() } } else if HitShortcut(event, Keys.Cancel, Keys.MovePreviousField, Keys.MoveNextField) { if b.blur != nil { b.blur(event.Key()) } } }) } // MouseHandler returns the mouse handler for this primitive. func (b *Button) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !b.InRect(event.Position()) { return false, nil } // Process mouse event. if action == MouseLeftClick { setFocus(b) if b.selected != nil { b.selected() } consumed = true } return }) }
package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // CheckBox implements a simple box for boolean values which can be checked and // unchecked. type CheckBox struct { *Box // Whether or not this box is checked. checked bool // The text to be displayed before the checkbox. label []byte // The text to be displayed after the checkbox. message []byte // The screen width of the label area. A value of 0 means use the width of // the label text. labelWidth int // The label color. labelColor tcell.Color // The label color when focused. labelColorFocused tcell.Color // The background color of the input area. fieldBackgroundColor tcell.Color // The background color of the input area when focused. fieldBackgroundColorFocused tcell.Color // The text color of the input area. fieldTextColor tcell.Color // The text color of the input area when focused. fieldTextColorFocused tcell.Color // An optional function which is called when the user changes the checked // state of this checkbox. changed func(checked bool) // An optional function which is called when the user indicated that they // are done entering text. The key which was pressed is provided (tab, // shift-tab, or escape). done func(tcell.Key) // A callback function set by the Form class and called when the user leaves // this form item. finished func(tcell.Key) // The rune to show when the checkbox is checked checkedRune rune sync.RWMutex } // NewCheckBox returns a new input field. func NewCheckBox() *CheckBox { return &CheckBox{ Box: NewBox(), labelColor: Styles.SecondaryTextColor, fieldBackgroundColor: Styles.ContrastBackgroundColor, fieldTextColor: Styles.PrimaryTextColor, checkedRune: Styles.CheckBoxCheckedRune, labelColorFocused: ColorUnset, fieldBackgroundColorFocused: ColorUnset, fieldTextColorFocused: ColorUnset, } } // SetChecked sets the state of the checkbox. func (c *CheckBox) SetChecked(checked bool) { c.Lock() defer c.Unlock() c.checked = checked } // SetCheckedRune sets the rune to show when the checkbox is checked. func (c *CheckBox) SetCheckedRune(rune rune) { c.Lock() defer c.Unlock() c.checkedRune = rune } // IsChecked returns whether or not the box is checked. func (c *CheckBox) IsChecked() bool { c.RLock() defer c.RUnlock() return c.checked } // SetLabel sets the text to be displayed before the input area. func (c *CheckBox) SetLabel(label string) { c.Lock() defer c.Unlock() c.label = []byte(label) } // GetLabel returns the text to be displayed before the input area. func (c *CheckBox) GetLabel() string { c.RLock() defer c.RUnlock() return string(c.label) } // SetMessage sets the text to be displayed after the checkbox func (c *CheckBox) SetMessage(message string) { c.Lock() defer c.Unlock() c.message = []byte(message) } // GetMessage returns the text to be displayed after the checkbox func (c *CheckBox) GetMessage() string { c.RLock() defer c.RUnlock() return string(c.message) } // SetLabelWidth sets the screen width of the label. A value of 0 will cause the // primitive to use the width of the label string. func (c *CheckBox) SetLabelWidth(width int) { c.Lock() defer c.Unlock() c.labelWidth = width } // SetLabelColor sets the color of the label. func (c *CheckBox) SetLabelColor(color tcell.Color) { c.Lock() defer c.Unlock() c.labelColor = color } // SetLabelColorFocused sets the color of the label when focused. func (c *CheckBox) SetLabelColorFocused(color tcell.Color) { c.Lock() defer c.Unlock() c.labelColorFocused = color } // SetFieldBackgroundColor sets the background color of the input area. func (c *CheckBox) SetFieldBackgroundColor(color tcell.Color) { c.Lock() defer c.Unlock() c.fieldBackgroundColor = color } // SetFieldBackgroundColorFocused sets the background color of the input area when focused. func (c *CheckBox) SetFieldBackgroundColorFocused(color tcell.Color) { c.Lock() defer c.Unlock() c.fieldBackgroundColorFocused = color } // SetFieldTextColor sets the text color of the input area. func (c *CheckBox) SetFieldTextColor(color tcell.Color) { c.Lock() defer c.Unlock() c.fieldTextColor = color } // SetFieldTextColorFocused sets the text color of the input area when focused. func (c *CheckBox) SetFieldTextColorFocused(color tcell.Color) { c.Lock() defer c.Unlock() c.fieldTextColorFocused = color } // GetFieldHeight returns the height of the field. func (c *CheckBox) GetFieldHeight() int { return 1 } // GetFieldWidth returns this primitive's field width. func (c *CheckBox) GetFieldWidth() int { c.RLock() defer c.RUnlock() if len(c.message) == 0 { return 1 } return 2 + len(c.message) } // SetChangedFunc sets a handler which is called when the checked state of this // checkbox was changed by the user. The handler function receives the new // state. func (c *CheckBox) SetChangedFunc(handler func(checked bool)) { c.Lock() defer c.Unlock() c.changed = handler } // SetDoneFunc sets a handler which is called when the user is done using the // checkbox. The callback function is provided with the key that was pressed, // which is one of the following: // // - KeyEscape: Abort text input. // - KeyTab: Move to the next field. // - KeyBacktab: Move to the previous field. func (c *CheckBox) SetDoneFunc(handler func(key tcell.Key)) { c.Lock() defer c.Unlock() c.done = handler } // SetFinishedFunc sets a callback invoked when the user leaves this form item. func (c *CheckBox) SetFinishedFunc(handler func(key tcell.Key)) { c.Lock() defer c.Unlock() c.finished = handler } // Draw draws this primitive onto the screen. func (c *CheckBox) Draw(screen tcell.Screen) { if !c.GetVisible() { return } c.Box.Draw(screen) c.Lock() defer c.Unlock() // Select colors labelColor := c.labelColor fieldBackgroundColor := c.fieldBackgroundColor fieldTextColor := c.fieldTextColor if c.GetFocusable().HasFocus() { if c.labelColorFocused != ColorUnset { labelColor = c.labelColorFocused } if c.fieldBackgroundColorFocused != ColorUnset { fieldBackgroundColor = c.fieldBackgroundColorFocused } if c.fieldTextColorFocused != ColorUnset { fieldTextColor = c.fieldTextColorFocused } } // Prepare x, y, width, height := c.GetInnerRect() rightLimit := x + width if height < 1 || rightLimit <= x { return } // Draw label. if c.labelWidth > 0 { labelWidth := c.labelWidth if labelWidth > rightLimit-x { labelWidth = rightLimit - x } Print(screen, c.label, x, y, labelWidth, AlignLeft, labelColor) x += labelWidth } else { _, drawnWidth := Print(screen, c.label, x, y, rightLimit-x, AlignLeft, labelColor) x += drawnWidth } // Draw checkbox. fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor).Foreground(fieldTextColor) checkedRune := c.checkedRune if !c.checked { checkedRune = ' ' } screen.SetContent(x, y, ' ', nil, fieldStyle) screen.SetContent(x+1, y, checkedRune, nil, fieldStyle) screen.SetContent(x+2, y, ' ', nil, fieldStyle) if len(c.message) > 0 { Print(screen, c.message, x+4, y, len(c.message), AlignLeft, labelColor) } } // InputHandler returns the handler for this primitive. func (c *CheckBox) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { if HitShortcut(event, Keys.Select, Keys.Select2) { c.Lock() c.checked = !c.checked c.Unlock() if c.changed != nil { c.changed(c.checked) } } else if HitShortcut(event, Keys.Cancel, Keys.MovePreviousField, Keys.MoveNextField) { if c.done != nil { c.done(event.Key()) } if c.finished != nil { c.finished(event.Key()) } } }) } // MouseHandler returns the mouse handler for this primitive. func (c *CheckBox) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return c.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { x, y := event.Position() _, rectY, _, _ := c.GetInnerRect() if !c.InRect(x, y) { return false, nil } // Process mouse event. if action == MouseLeftClick && y == rectY { setFocus(c) c.checked = !c.checked if c.changed != nil { c.changed(c.checked) } consumed = true } return }) }
package cview import "sync" // ContextMenu is a menu that appears upon user interaction, such as right // clicking or pressing Alt+Enter. type ContextMenu struct { parent Primitive item int open bool drag bool list *List x, y int selected func(int, string, rune) l sync.RWMutex } // NewContextMenu returns a new context menu. func NewContextMenu(parent Primitive) *ContextMenu { return &ContextMenu{ parent: parent, } } func (c *ContextMenu) initializeList() { if c.list != nil { return } c.list = NewList() c.list.ShowSecondaryText(false) c.list.SetHover(true) c.list.SetWrapAround(true) c.list.ShowFocus(false) c.list.SetBorder(true) c.list.SetPadding( Styles.ContextMenuPaddingTop, Styles.ContextMenuPaddingBottom, Styles.ContextMenuPaddingLeft, Styles.ContextMenuPaddingRight) } // ContextMenuList returns the underlying List of the context menu. func (c *ContextMenu) ContextMenuList() *List { c.l.Lock() defer c.l.Unlock() c.initializeList() return c.list } // AddContextItem adds an item to the context menu. Adding an item with no text // or shortcut will add a divider. func (c *ContextMenu) AddContextItem(text string, shortcut rune, selected func(index int)) { c.l.Lock() defer c.l.Unlock() c.initializeList() item := NewListItem(text) item.SetShortcut(shortcut) item.SetSelectedFunc(c.wrap(selected)) c.list.AddItem(item) if text == "" && shortcut == 0 { c.list.Lock() index := len(c.list.items) - 1 c.list.items[index].disabled = true c.list.Unlock() } } func (c *ContextMenu) wrap(f func(index int)) func() { return func() { f(c.item) } } // ClearContextMenu removes all items from the context menu. func (c *ContextMenu) ClearContextMenu() { c.l.Lock() defer c.l.Unlock() c.initializeList() c.list.Clear() } // SetContextSelectedFunc sets the function which is called when the user // selects a context menu item. The function receives the item's index in the // menu (starting with 0), its text and its shortcut rune. SetSelectedFunc must // be called before the context menu is shown. func (c *ContextMenu) SetContextSelectedFunc(handler func(index int, text string, shortcut rune)) { c.l.Lock() defer c.l.Unlock() c.selected = handler } // ShowContextMenu shows the context menu. Provide -1 for both to position on // the selected item, or specify a position. func (c *ContextMenu) ShowContextMenu(item int, x int, y int, setFocus func(Primitive)) { c.l.Lock() defer c.l.Unlock() c.show(item, x, y, setFocus) } // HideContextMenu hides the context menu. func (c *ContextMenu) HideContextMenu(setFocus func(Primitive)) { c.l.Lock() defer c.l.Unlock() c.hide(setFocus) } // ContextMenuVisible returns whether or not the context menu is visible. func (c *ContextMenu) ContextMenuVisible() bool { c.l.Lock() defer c.l.Unlock() return c.open } func (c *ContextMenu) show(item int, x int, y int, setFocus func(Primitive)) { c.initializeList() if len(c.list.items) == 0 { return } c.open = true c.item = item c.x, c.y = x, y c.list.Lock() for i, item := range c.list.items { if !item.disabled { c.list.currentItem = i break } } c.list.Unlock() c.list.SetSelectedFunc(func(index int, item *ListItem) { c.l.Lock() // A context item was selected. Close the menu. c.hide(setFocus) if c.selected != nil { c.l.Unlock() c.selected(index, string(item.mainText), item.shortcut) } else { c.l.Unlock() } }) c.list.SetDoneFunc(func() { c.l.Lock() defer c.l.Unlock() c.hide(setFocus) }) setFocus(c.list) } func (c *ContextMenu) hide(setFocus func(Primitive)) { c.initializeList() c.open = false if c.list.HasFocus() { setFocus(c.parent) } }
package cview import ( "strings" "sync" "github.com/gdamore/tcell/v2" "github.com/mattn/go-runewidth" ) // DropDownOption is one option that can be selected in a drop-down primitive. type DropDownOption struct { text string // The text to be displayed in the drop-down. selected func(index int, option *DropDownOption) // The (optional) callback for when this option was selected. reference interface{} // An optional reference object. sync.RWMutex } // NewDropDownOption returns a new option for a dropdown. func NewDropDownOption(text string) *DropDownOption { return &DropDownOption{text: text} } // GetText returns the text of this dropdown option. func (d *DropDownOption) GetText() string { d.RLock() defer d.RUnlock() return d.text } // SetText returns the text of this dropdown option. func (d *DropDownOption) SetText(text string) { d.text = text } // SetSelectedFunc sets the handler to be called when this option is selected. func (d *DropDownOption) SetSelectedFunc(handler func(index int, option *DropDownOption)) { d.selected = handler } // GetReference returns the reference object of this dropdown option. func (d *DropDownOption) GetReference() interface{} { d.RLock() defer d.RUnlock() return d.reference } // SetReference allows you to store a reference of any type in this option. func (d *DropDownOption) SetReference(reference interface{}) { d.reference = reference } // DropDown implements a selection widget whose options become visible in a // drop-down list when activated. type DropDown struct { *Box // The options from which the user can choose. options []*DropDownOption // Strings to be placed before and after each drop-down option. optionPrefix, optionSuffix string // The index of the currently selected option. Negative if no option is // currently selected. currentOption int // Strings to be placed before and after the current option. currentOptionPrefix, currentOptionSuffix string // The text to be displayed when no option has yet been selected. noSelection string // Set to true if the options are visible and selectable. open bool // The runes typed so far to directly access one of the list items. prefix string // The list element for the options. list *List // The text to be displayed before the input area. label string // The label color. labelColor tcell.Color // The label color when focused. labelColorFocused tcell.Color // The background color of the input area. fieldBackgroundColor tcell.Color // The background color of the input area when focused. fieldBackgroundColorFocused tcell.Color // The text color of the input area. fieldTextColor tcell.Color // The text color of the input area when focused. fieldTextColorFocused tcell.Color // The color for prefixes. prefixTextColor tcell.Color // The screen width of the label area. A value of 0 means use the width of // the label text. labelWidth int // The screen width of the input area. A value of 0 means extend as much as // possible. fieldWidth int // An optional function which is called when the user indicated that they // are done selecting options. The key which was pressed is provided (tab, // shift-tab, or escape). done func(tcell.Key) // A callback function set by the Form class and called when the user leaves // this form item. finished func(tcell.Key) // A callback function which is called when the user changes the drop-down's // selection. selected func(index int, option *DropDownOption) // Set to true when mouse dragging is in progress. dragging bool // The chars to show when the option's text gets shortened. abbreviationChars string // The symbol to draw at the end of the field. dropDownSymbol rune sync.RWMutex } // NewDropDown returns a new drop-down. func NewDropDown() *DropDown { list := NewList() list.ShowSecondaryText(false) list.SetMainTextColor(Styles.PrimitiveBackgroundColor) list.SetSelectedTextColor(Styles.PrimitiveBackgroundColor) list.SetSelectedBackgroundColor(Styles.PrimaryTextColor) list.SetHighlightFullLine(true) list.SetBackgroundColor(Styles.MoreContrastBackgroundColor) d := &DropDown{ Box: NewBox(), currentOption: -1, list: list, labelColor: Styles.SecondaryTextColor, fieldBackgroundColor: Styles.ContrastBackgroundColor, fieldTextColor: Styles.PrimaryTextColor, prefixTextColor: Styles.ContrastSecondaryTextColor, dropDownSymbol: Styles.DropDownSymbol, abbreviationChars: Styles.DropDownAbbreviationChars, labelColorFocused: ColorUnset, fieldBackgroundColorFocused: ColorUnset, fieldTextColorFocused: ColorUnset, } d.focus = d return d } // SetDropDownSymbolRune sets the rune to be drawn at the end of the dropdown field // to indicate that this field is a dropdown. func (d *DropDown) SetDropDownSymbolRune(symbol rune) { d.Lock() defer d.Unlock() d.dropDownSymbol = symbol } // SetCurrentOption sets the index of the currently selected option. This may // be a negative value to indicate that no option is currently selected. Calling // this function will also trigger the "selected" callback (if there is one). func (d *DropDown) SetCurrentOption(index int) { d.Lock() defer d.Unlock() if index >= 0 && index < len(d.options) { d.currentOption = index d.list.SetCurrentItem(index) if d.selected != nil { d.Unlock() d.selected(index, d.options[index]) d.Lock() } if d.options[index].selected != nil { d.Unlock() d.options[index].selected(index, d.options[index]) d.Lock() } } else { d.currentOption = -1 d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item". if d.selected != nil { d.Unlock() d.selected(-1, nil) d.Lock() } } } // GetCurrentOption returns the index of the currently selected option as well // as the option itself. If no option was selected, -1 and nil is returned. func (d *DropDown) GetCurrentOption() (int, *DropDownOption) { d.RLock() defer d.RUnlock() var option *DropDownOption if d.currentOption >= 0 && d.currentOption < len(d.options) { option = d.options[d.currentOption] } return d.currentOption, option } // SetTextOptions sets the text to be placed before and after each drop-down // option (prefix/suffix), the text placed before and after the currently // selected option (currentPrefix/currentSuffix) as well as the text to be // displayed when no option is currently selected. Per default, all of these // strings are empty. func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) { d.Lock() defer d.Unlock() d.currentOptionPrefix = currentPrefix d.currentOptionSuffix = currentSuffix d.noSelection = noSelection d.optionPrefix = prefix d.optionSuffix = suffix for index := 0; index < d.list.GetItemCount(); index++ { d.list.SetItemText(index, prefix+d.options[index].text+suffix, "") } } // SetLabel sets the text to be displayed before the input area. func (d *DropDown) SetLabel(label string) { d.Lock() defer d.Unlock() d.label = label } // GetLabel returns the text to be displayed before the input area. func (d *DropDown) GetLabel() string { d.RLock() defer d.RUnlock() return d.label } // SetLabelWidth sets the screen width of the label. A value of 0 will cause the // primitive to use the width of the label string. func (d *DropDown) SetLabelWidth(width int) { d.Lock() defer d.Unlock() d.labelWidth = width } // SetLabelColor sets the color of the label. func (d *DropDown) SetLabelColor(color tcell.Color) { d.Lock() defer d.Unlock() d.labelColor = color } // SetLabelColorFocused sets the color of the label when focused. func (d *DropDown) SetLabelColorFocused(color tcell.Color) { d.Lock() defer d.Unlock() d.labelColorFocused = color } // SetFieldBackgroundColor sets the background color of the options area. func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) { d.Lock() defer d.Unlock() d.fieldBackgroundColor = color } // SetFieldBackgroundColorFocused sets the background color of the options area when focused. func (d *DropDown) SetFieldBackgroundColorFocused(color tcell.Color) { d.Lock() defer d.Unlock() d.fieldBackgroundColorFocused = color } // SetFieldTextColor sets the text color of the options area. func (d *DropDown) SetFieldTextColor(color tcell.Color) { d.Lock() defer d.Unlock() d.fieldTextColor = color } // SetFieldTextColorFocused sets the text color of the options area when focused. func (d *DropDown) SetFieldTextColorFocused(color tcell.Color) { d.Lock() defer d.Unlock() d.fieldTextColorFocused = color } // SetDropDownTextColor sets text color of the drop-down list. func (d *DropDown) SetDropDownTextColor(color tcell.Color) { d.Lock() defer d.Unlock() d.list.SetMainTextColor(color) } // SetDropDownBackgroundColor sets the background color of the drop-down list. func (d *DropDown) SetDropDownBackgroundColor(color tcell.Color) { d.Lock() defer d.Unlock() d.list.SetBackgroundColor(color) } // SetDropDownSelectedTextColor sets the text color of the selected option in // the drop-down list. func (d *DropDown) SetDropDownSelectedTextColor(color tcell.Color) { d.Lock() defer d.Unlock() d.list.SetSelectedTextColor(color) } // SetDropDownSelectedBackgroundColor sets the background color of the selected // option in the drop-down list. func (d *DropDown) SetDropDownSelectedBackgroundColor(color tcell.Color) { d.Lock() defer d.Unlock() d.list.SetSelectedBackgroundColor(color) } // SetPrefixTextColor sets the color of the prefix string. The prefix string is // shown when the user starts typing text, which directly selects the first // option that starts with the typed string. func (d *DropDown) SetPrefixTextColor(color tcell.Color) { d.Lock() defer d.Unlock() d.prefixTextColor = color } // SetFieldWidth sets the screen width of the options area. A value of 0 means // extend to as long as the longest option text. func (d *DropDown) SetFieldWidth(width int) { d.Lock() defer d.Unlock() d.fieldWidth = width } // GetFieldHeight returns the height of the field. func (d *DropDown) GetFieldHeight() int { return 1 } // GetFieldWidth returns this primitive's field screen width. func (d *DropDown) GetFieldWidth() int { d.RLock() defer d.RUnlock() return d.getFieldWidth() } func (d *DropDown) getFieldWidth() int { if d.fieldWidth > 0 { return d.fieldWidth } fieldWidth := 0 for _, option := range d.options { width := TaggedStringWidth(option.text) if width > fieldWidth { fieldWidth = width } } fieldWidth += len(d.optionPrefix) + len(d.optionSuffix) fieldWidth += len(d.currentOptionPrefix) + len(d.currentOptionSuffix) fieldWidth += 3 // space + dropDownSymbol + space return fieldWidth } // AddOptionsSimple adds new selectable options to this drop-down. func (d *DropDown) AddOptionsSimple(options ...string) { optionsToAdd := make([]*DropDownOption, len(options)) for i, option := range options { optionsToAdd[i] = NewDropDownOption(option) } d.AddOptions(optionsToAdd...) } // AddOptions adds new selectable options to this drop-down. func (d *DropDown) AddOptions(options ...*DropDownOption) { d.Lock() defer d.Unlock() d.addOptions(options...) } func (d *DropDown) addOptions(options ...*DropDownOption) { d.options = append(d.options, options...) for _, option := range options { d.list.AddItem(NewListItem(d.optionPrefix + option.text + d.optionSuffix)) } } // SetOptionsSimple replaces all current options with the ones provided and installs // one callback function which is called when one of the options is selected. // It will be called with the option's index and the option itself // The "selected" parameter may be nil. func (d *DropDown) SetOptionsSimple(selected func(index int, option *DropDownOption), options ...string) { optionsToSet := make([]*DropDownOption, len(options)) for i, option := range options { optionsToSet[i] = NewDropDownOption(option) } d.SetOptions(selected, optionsToSet...) } // SetOptions replaces all current options with the ones provided and installs // one callback function which is called when one of the options is selected. // It will be called with the option's index and the option itself. // The "selected" parameter may be nil. func (d *DropDown) SetOptions(selected func(index int, option *DropDownOption), options ...*DropDownOption) { d.Lock() defer d.Unlock() d.list.Clear() d.options = nil d.addOptions(options...) d.selected = selected } // SetChangedFunc sets a handler which is called when the user changes the // focused drop-down option. The handler is provided with the selected option's // index and the option itself. If "no option" was selected, these values are // -1 and nil. func (d *DropDown) SetChangedFunc(handler func(index int, option *DropDownOption)) { d.list.SetChangedFunc(func(index int, item *ListItem) { handler(index, d.options[index]) }) } // SetSelectedFunc sets a handler which is called when the user selects a // drop-down's option. This handler will be called in addition and prior to // an option's optional individual handler. The handler is provided with the // selected option's index and the option itself. If "no option" was selected, these values // are -1 and nil. func (d *DropDown) SetSelectedFunc(handler func(index int, option *DropDownOption)) { d.Lock() defer d.Unlock() d.selected = handler } // SetDoneFunc sets a handler which is called when the user is done selecting // options. The callback function is provided with the key that was pressed, // which is one of the following: // // - KeyEscape: Abort selection. // - KeyTab: Move to the next field. // - KeyBacktab: Move to the previous field. func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) { d.Lock() defer d.Unlock() d.done = handler } // SetFinishedFunc sets a callback invoked when the user leaves this form item. func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) { d.Lock() defer d.Unlock() d.finished = handler } // Draw draws this primitive onto the screen. func (d *DropDown) Draw(screen tcell.Screen) { d.Box.Draw(screen) hasFocus := d.GetFocusable().HasFocus() d.Lock() defer d.Unlock() // Select colors labelColor := d.labelColor fieldBackgroundColor := d.fieldBackgroundColor fieldTextColor := d.fieldTextColor if hasFocus { if d.labelColorFocused != ColorUnset { labelColor = d.labelColorFocused } if d.fieldBackgroundColorFocused != ColorUnset { fieldBackgroundColor = d.fieldBackgroundColorFocused } if d.fieldTextColorFocused != ColorUnset { fieldTextColor = d.fieldTextColorFocused } } // Prepare. x, y, width, height := d.GetInnerRect() rightLimit := x + width if height < 1 || rightLimit <= x { return } // Draw label. if d.labelWidth > 0 { labelWidth := d.labelWidth if labelWidth > rightLimit-x { labelWidth = rightLimit - x } Print(screen, []byte(d.label), x, y, labelWidth, AlignLeft, labelColor) x += labelWidth } else { _, drawnWidth := Print(screen, []byte(d.label), x, y, rightLimit-x, AlignLeft, labelColor) x += drawnWidth } // What's the longest option text? maxWidth := 0 optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix) for _, option := range d.options { strWidth := TaggedStringWidth(option.text) + optionWrapWidth if strWidth > maxWidth { maxWidth = strWidth } } // Draw selection area. fieldWidth := d.getFieldWidth() if fieldWidth == 0 { fieldWidth = maxWidth if d.currentOption < 0 { noSelectionWidth := TaggedStringWidth(d.noSelection) if noSelectionWidth > fieldWidth { fieldWidth = noSelectionWidth } } else if d.currentOption < len(d.options) { currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix) if currentOptionWidth > fieldWidth { fieldWidth = currentOptionWidth } } } if rightLimit-x < fieldWidth { fieldWidth = rightLimit - x } fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor) for index := 0; index < fieldWidth; index++ { screen.SetContent(x+index, y, ' ', nil, fieldStyle) } // Draw selected text. if d.open && len(d.prefix) > 0 { // Show the prefix. currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix) prefixWidth := runewidth.StringWidth(d.prefix) listItemText := d.options[d.list.GetCurrentItemIndex()].text Print(screen, []byte(d.currentOptionPrefix), x, y, fieldWidth, AlignLeft, fieldTextColor) Print(screen, []byte(d.prefix), x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor) if len(d.prefix) < len(listItemText) { Print(screen, []byte(listItemText[len(d.prefix):]+d.currentOptionSuffix), x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, fieldTextColor) } } else { color := fieldTextColor text := d.noSelection if d.currentOption >= 0 && d.currentOption < len(d.options) { text = d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix } // Abbreviate text when not fitting if fieldWidth > len(d.abbreviationChars)+3 && len(text) > fieldWidth { text = text[0:fieldWidth-3-len(d.abbreviationChars)] + d.abbreviationChars } // Just show the current selection. Print(screen, []byte(text), x, y, fieldWidth, AlignLeft, color) } // Draw drop-down symbol screen.SetContent(x+fieldWidth-2, y, d.dropDownSymbol, nil, new(tcell.Style).Foreground(fieldTextColor).Background(fieldBackgroundColor)) // Draw options list. if hasFocus && d.open { // We prefer to drop-down but if there is no space, maybe drop up? lx := x ly := y + 1 lheight := len(d.options) _, sheight := screen.Size() if ly+lheight >= sheight && ly-2 > lheight-ly { ly = y - lheight if ly < 0 { ly = 0 } } if ly+lheight >= sheight { lheight = sheight - ly } lwidth := maxWidth if d.list.scrollBarVisibility == ScrollBarAlways || (d.list.scrollBarVisibility == ScrollBarAuto && len(d.options) > lheight) { lwidth++ // Add space for scroll bar } if lwidth < fieldWidth { lwidth = fieldWidth } d.list.SetRect(lx, ly, lwidth, lheight) d.list.Draw(screen) } } // InputHandler returns the handler for this primitive. func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { // Process key event. switch key := event.Key(); key { case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown: d.Lock() defer d.Unlock() d.prefix = "" // If the first key was a letter already, it becomes part of the prefix. if r := event.Rune(); key == tcell.KeyRune && r != ' ' { d.prefix += string(r) d.evalPrefix() } d.openList(setFocus) case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab: if d.done != nil { d.done(key) } if d.finished != nil { d.finished(key) } } }) } // evalPrefix selects an item in the drop-down list based on the current prefix. func (d *DropDown) evalPrefix() { if len(d.prefix) > 0 { for index, option := range d.options { if strings.HasPrefix(strings.ToLower(option.text), d.prefix) { d.list.SetCurrentItem(index) return } } // Prefix does not match any item. Remove last rune. r := []rune(d.prefix) d.prefix = string(r[:len(r)-1]) } } // openList hands control over to the embedded List primitive. func (d *DropDown) openList(setFocus func(Primitive)) { d.open = true optionBefore := d.currentOption d.list.SetSelectedFunc(func(index int, item *ListItem) { if d.dragging { return // If we're dragging the mouse, we don't want to trigger any events. } // An option was selected. Close the list again. d.currentOption = index d.closeList(setFocus) // Trigger "selected" event. if d.selected != nil { d.selected(d.currentOption, d.options[d.currentOption]) } if d.options[d.currentOption].selected != nil { d.options[d.currentOption].selected(d.currentOption, d.options[d.currentOption]) } }) d.list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyRune { d.prefix += string(event.Rune()) d.evalPrefix() } else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 { if len(d.prefix) > 0 { r := []rune(d.prefix) d.prefix = string(r[:len(r)-1]) } d.evalPrefix() } else if event.Key() == tcell.KeyEscape { d.currentOption = optionBefore d.list.SetCurrentItem(d.currentOption) d.closeList(setFocus) if d.selected != nil { if d.currentOption > -1 { d.selected(d.currentOption, d.options[d.currentOption]) } } } else { d.prefix = "" } return event }) setFocus(d.list) } // closeList closes the embedded List element by hiding it and removing focus // from it. func (d *DropDown) closeList(setFocus func(Primitive)) { d.open = false if d.list.HasFocus() { setFocus(d) } } // Focus is called by the application when the primitive receives focus. func (d *DropDown) Focus(delegate func(p Primitive)) { d.Box.Focus(delegate) if d.open { delegate(d.list) } } // HasFocus returns whether or not this primitive has focus. func (d *DropDown) HasFocus() bool { d.RLock() defer d.RUnlock() if d.open { return d.list.HasFocus() } return d.hasFocus } // MouseHandler returns the mouse handler for this primitive. func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { // Was the mouse event in the drop-down box itself (or on its label)? x, y := event.Position() _, rectY, _, _ := d.GetInnerRect() inRect := y == rectY if !d.open && !inRect { return d.InRect(x, y), nil // No, and it's not expanded either. Ignore. } // Handle dragging. Clicks are implicitly handled by this logic. switch action { case MouseLeftDown: consumed = d.open || inRect capture = d if !d.open { d.openList(setFocus) d.dragging = true } else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed { d.closeList(setFocus) // Close drop-down if clicked outside of it. } case MouseMove: if d.dragging { // We pretend it's a left click so we can see the selection during // dragging. Because we don't act upon it, it's not a problem. d.list.MouseHandler()(MouseLeftClick, event, setFocus) consumed = true capture = d } case MouseLeftUp: if d.dragging { d.dragging = false d.list.MouseHandler()(MouseLeftClick, event, setFocus) consumed = true } } return }) }
package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // Configuration values. const ( FlexRow = iota FlexColumn ) // flexItem holds layout options for one item. type flexItem struct { Item Primitive // The item to be positioned. May be nil for an empty item. FixedSize int // The item's fixed size which may not be changed, 0 if it has no fixed size. Proportion int // The item's proportion. Focus bool // Whether or not this item attracts the layout's focus. } // Flex is a basic implementation of the Flexbox layout. The contained // primitives are arranged horizontally or vertically. The way they are // distributed along that dimension depends on their layout settings, which is // either a fixed length or a proportional length. See AddItem() for details. type Flex struct { *Box // The items to be positioned. items []*flexItem // FlexRow or FlexColumn. direction int // If set to true, Flex will use the entire screen as its available space // instead its box dimensions. fullScreen bool sync.RWMutex } // NewFlex returns a new flexbox layout container with no primitives and its // direction set to FlexColumn. To add primitives to this layout, see AddItem(). // To change the direction, see SetDirection(). // // Note that Flex will have a transparent background by default so that any nil // flex items will show primitives behind the Flex. // To disable this transparency: // // flex.SetBackgroundTransparent(false) func NewFlex() *Flex { f := &Flex{ Box: NewBox(), direction: FlexColumn, } f.SetBackgroundTransparent(true) f.focus = f return f } // GetDirection returns the direction in which the contained primitives are // distributed. This can be either FlexColumn (default) or FlexRow. func (f *Flex) GetDirection() int { f.RLock() defer f.RUnlock() return f.direction } // SetDirection sets the direction in which the contained primitives are // distributed. This can be either FlexColumn (default) or FlexRow. func (f *Flex) SetDirection(direction int) { f.Lock() defer f.Unlock() f.direction = direction } // SetFullScreen sets the flag which, when true, causes the flex layout to use // the entire screen space instead of whatever size it is currently assigned to. func (f *Flex) SetFullScreen(fullScreen bool) { f.Lock() defer f.Unlock() f.fullScreen = fullScreen } // AddItem adds a new item to the container. The "fixedSize" argument is a width // or height that may not be changed by the layout algorithm. A value of 0 means // that its size is flexible and may be changed. The "proportion" argument // defines the relative size of the item compared to other flexible-size items. // For example, items with a proportion of 2 will be twice as large as items // with a proportion of 1. The proportion must be at least 1 if fixedSize == 0 // (ignored otherwise). // // If "focus" is set to true, the item will receive focus when the Flex // primitive receives focus. If multiple items have the "focus" flag set to // true, the first one will receive focus. // // You can provide a nil value for the primitive. This will fill the empty // screen space with the default background color. To show content behind the // space, add a Box with a transparent background instead. func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) { f.Lock() defer f.Unlock() if item == nil { item = NewBox() } f.items = append(f.items, &flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus}) } // AddItemAtIndex adds an item to the flex at a given index. // For more information see AddItem. func (f *Flex) AddItemAtIndex(index int, item Primitive, fixedSize, proportion int, focus bool) { f.Lock() defer f.Unlock() newItem := &flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus} if index == 0 { f.items = append([]*flexItem{newItem}, f.items...) } else { f.items = append(f.items[:index], append([]*flexItem{newItem}, f.items[index:]...)...) } } // RemoveItem removes all items for the given primitive from the container, // keeping the order of the remaining items intact. func (f *Flex) RemoveItem(p Primitive) { f.Lock() defer f.Unlock() for index := len(f.items) - 1; index >= 0; index-- { if f.items[index].Item == p { f.items = append(f.items[:index], f.items[index+1:]...) } } } // ResizeItem sets a new size for the item(s) with the given primitive. If there // are multiple Flex items with the same primitive, they will all receive the // same size. For details regarding the size parameters, see AddItem(). func (f *Flex) ResizeItem(p Primitive, fixedSize, proportion int) { f.Lock() defer f.Unlock() for _, item := range f.items { if item.Item == p { item.FixedSize = fixedSize item.Proportion = proportion } } } // Draw draws this primitive onto the screen. func (f *Flex) Draw(screen tcell.Screen) { if !f.GetVisible() { return } f.Box.Draw(screen) f.Lock() defer f.Unlock() // Calculate size and position of the items. // Do we use the entire screen? if f.fullScreen { width, height := screen.Size() f.SetRect(0, 0, width, height) } // How much space can we distribute? x, y, width, height := f.GetInnerRect() var proportionSum int distSize := width if f.direction == FlexRow { distSize = height } for _, item := range f.items { if item.FixedSize > 0 { distSize -= item.FixedSize } else { proportionSum += item.Proportion } } // Calculate positions and draw items. pos := x if f.direction == FlexRow { pos = y } for _, item := range f.items { size := item.FixedSize if size <= 0 { if proportionSum > 0 { size = distSize * item.Proportion / proportionSum distSize -= size proportionSum -= item.Proportion } else { size = 0 } } if item.Item != nil { if f.direction == FlexColumn { item.Item.SetRect(pos, y, size, height) } else { item.Item.SetRect(x, pos, width, size) } } pos += size if item.Item != nil { if item.Item.GetFocusable().HasFocus() { defer item.Item.Draw(screen) } else { item.Item.Draw(screen) } } } } // Focus is called when this primitive receives focus. func (f *Flex) Focus(delegate func(p Primitive)) { f.Lock() for _, item := range f.items { if item.Item != nil && item.Focus { f.Unlock() delegate(item.Item) return } } f.Unlock() } // HasFocus returns whether or not this primitive has focus. func (f *Flex) HasFocus() bool { f.RLock() defer f.RUnlock() for _, item := range f.items { if item.Item != nil && item.Item.GetFocusable().HasFocus() { return true } } return false } // MouseHandler returns the mouse handler for this primitive. func (f *Flex) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !f.InRect(event.Position()) { return false, nil } // Pass mouse events along to the first child item that takes it. for _, item := range f.items { if item.Item == nil { continue } consumed, capture = item.Item.MouseHandler()(action, event, setFocus) if consumed { return } } return }) }
package cview import "sync" // Focusable provides a method which determines if a primitive has focus. // Composed primitives may be focused based on the focused state of their // contained primitives. type Focusable interface { HasFocus() bool } type focusElement struct { primitive Primitive disabled bool } // FocusManager manages application focus. type FocusManager struct { elements []*focusElement focused int wrapAround bool setFocus func(p Primitive) sync.RWMutex } // NewFocusManager returns a new FocusManager object. func NewFocusManager(setFocus func(p Primitive)) *FocusManager { return &FocusManager{setFocus: setFocus} } // SetWrapAround sets the flag that determines whether navigation will wrap // around. That is, navigating forwards on the last field will move the // selection to the first field (similarly in the other direction). If set to // false, the focus won't change when navigating forwards on the last element // or navigating backwards on the first element. func (f *FocusManager) SetWrapAround(wrapAround bool) { f.Lock() defer f.Unlock() f.wrapAround = wrapAround } // Add adds an element to the focus handler. func (f *FocusManager) Add(p ...Primitive) { f.Lock() defer f.Unlock() for _, primitive := range p { f.elements = append(f.elements, &focusElement{primitive: primitive}) } } // AddAt adds an element to the focus handler at the specified index. func (f *FocusManager) AddAt(index int, p Primitive) { f.Lock() defer f.Unlock() if index < 0 || index > len(f.elements) { panic("index out of range") } element := &focusElement{primitive: p} if index == len(f.elements) { f.elements = append(f.elements, element) return } f.elements = append(f.elements[:index+1], f.elements[index:]...) f.elements[index] = element } // Focus focuses the provided element. func (f *FocusManager) Focus(p Primitive) { f.Lock() defer f.Unlock() for i, element := range f.elements { if p == element.primitive && !element.disabled { f.focused = i break } } f.setFocus(f.elements[f.focused].primitive) } // FocusPrevious focuses the previous element. func (f *FocusManager) FocusPrevious() { f.Lock() defer f.Unlock() f.focused-- f.updateFocusIndex(true) f.setFocus(f.elements[f.focused].primitive) } // FocusNext focuses the next element. func (f *FocusManager) FocusNext() { f.Lock() defer f.Unlock() f.focused++ f.updateFocusIndex(false) f.setFocus(f.elements[f.focused].primitive) } // FocusAt focuses the element at the provided index. func (f *FocusManager) FocusAt(index int) { f.Lock() defer f.Unlock() f.focused = index f.setFocus(f.elements[f.focused].primitive) } // GetFocusIndex returns the index of the currently focused element. func (f *FocusManager) GetFocusIndex() int { f.Lock() defer f.Unlock() return f.focused } // GetFocusedPrimitive returns the currently focused primitive. func (f *FocusManager) GetFocusedPrimitive() Primitive { f.Lock() defer f.Unlock() return f.elements[f.focused].primitive } func (f *FocusManager) updateFocusIndex(decreasing bool) { for i := 0; i < len(f.elements); i++ { if f.focused < 0 { if f.wrapAround { f.focused = len(f.elements) - 1 } else { f.focused = 0 } } else if f.focused >= len(f.elements) { if f.wrapAround { f.focused = 0 } else { f.focused = len(f.elements) - 1 } } item := f.elements[f.focused] if !item.disabled { break } if decreasing { f.focused-- } else { f.focused++ } } } // Transform modifies the current focus. func (f *FocusManager) Transform(tr Transformation) { var decreasing bool switch tr { case TransformFirstItem: f.focused = 0 decreasing = true case TransformLastItem: f.focused = len(f.elements) - 1 case TransformPreviousItem: f.focused-- decreasing = true case TransformNextItem: f.focused++ } f.updateFocusIndex(decreasing) }
package cview import ( "reflect" "sync" "github.com/gdamore/tcell/v2" ) // DefaultFormFieldWidth is the default field screen width of form elements // whose field width is flexible (0). This is used in the Form class for // horizontal layouts. var DefaultFormFieldWidth = 10 // FormItemAttributes is a set of attributes to be applied. type FormItemAttributes struct { // The screen width of the label. A value of 0 will cause the primitive to // use the width of the label string. LabelWidth int BackgroundColor tcell.Color LabelColor tcell.Color LabelColorFocused tcell.Color FieldBackgroundColor tcell.Color FieldBackgroundColorFocused tcell.Color FieldTextColor tcell.Color FieldTextColorFocused tcell.Color FinishedFunc func(key tcell.Key) } // FormItem is the interface all form items must implement to be able to be // included in a form. type FormItem interface { Primitive // GetLabel returns the item's label text. GetLabel() string // SetLabelWidth sets the screen width of the label. A value of 0 will cause the // primitive to use the width of the label string. SetLabelWidth(int) // SetLabelColor sets the color of the label. SetLabelColor(tcell.Color) // SetLabelColor sets the color of the label when focused. SetLabelColorFocused(tcell.Color) // GetFieldWidth returns the width of the form item's field (the area which // is manipulated by the user) in number of screen cells. A value of 0 // indicates the the field width is flexible and may use as much space as // required. GetFieldWidth() int // GetFieldHeight returns the height of the form item. GetFieldHeight() int // SetFieldTextColor sets the text color of the input area. SetFieldTextColor(tcell.Color) // SetFieldTextColorFocused sets the text color of the input area when focused. SetFieldTextColorFocused(tcell.Color) // SetFieldBackgroundColor sets the background color of the input area. SetFieldBackgroundColor(tcell.Color) // SetFieldBackgroundColor sets the background color of the input area when focused. SetFieldBackgroundColorFocused(tcell.Color) // SetBackgroundColor sets the background color of the form item. SetBackgroundColor(tcell.Color) // SetFinishedFunc sets a callback invoked when the user leaves the form item. SetFinishedFunc(func(key tcell.Key)) } // Form allows you to combine multiple one-line form elements into a vertical // or horizontal layout. Form elements include types such as InputField or // CheckBox. These elements can be optionally followed by one or more buttons // for which you can define form-wide actions (e.g. Save, Clear, Cancel). type Form struct { *Box // The items of the form (one row per item). items []FormItem // The buttons of the form. buttons []*Button // If set to true, instead of position items and buttons from top to bottom, // they are positioned from left to right. horizontal bool // The alignment of the buttons. buttonsAlign int // The number of empty rows between items. itemPadding int // The index of the item or button which has focus. (Items are counted first, // buttons are counted last.) This is only used when the form itself receives // focus so that the last element that had focus keeps it. focusedElement int // Whether or not navigating the form will wrap around. wrapAround bool // The label color. labelColor tcell.Color // The label color when focused. labelColorFocused tcell.Color // The background color of the input area. fieldBackgroundColor tcell.Color // The background color of the input area when focused. fieldBackgroundColorFocused tcell.Color // The text color of the input area. fieldTextColor tcell.Color // The text color of the input area when focused. fieldTextColorFocused tcell.Color // The background color of the buttons. buttonBackgroundColor tcell.Color // The background color of the buttons when focused. buttonBackgroundColorFocused tcell.Color // The color of the button text. buttonTextColor tcell.Color // The color of the button text when focused. buttonTextColorFocused tcell.Color // An optional function which is called when the user hits Escape. cancel func() sync.RWMutex } // NewForm returns a new form. func NewForm() *Form { box := NewBox() box.SetPadding(1, 1, 1, 1) f := &Form{ Box: box, itemPadding: 1, labelColor: Styles.SecondaryTextColor, fieldBackgroundColor: Styles.ContrastBackgroundColor, fieldTextColor: Styles.PrimaryTextColor, buttonBackgroundColor: Styles.ContrastBackgroundColor, buttonBackgroundColorFocused: Styles.PrimaryTextColor, buttonTextColor: Styles.PrimaryTextColor, buttonTextColorFocused: Styles.InverseTextColor, labelColorFocused: ColorUnset, fieldTextColorFocused: ColorUnset, fieldBackgroundColorFocused: ColorUnset, } f.focus = f return f } // SetItemPadding sets the number of empty rows between form items for vertical // layouts and the number of empty cells between form items for horizontal // layouts. func (f *Form) SetItemPadding(padding int) { f.Lock() defer f.Unlock() f.itemPadding = padding } // SetHorizontal sets the direction the form elements are laid out. If set to // true, instead of positioning them from top to bottom (the default), they are // positioned from left to right, moving into the next row if there is not // enough space. func (f *Form) SetHorizontal(horizontal bool) { f.Lock() defer f.Unlock() f.horizontal = horizontal } // SetLabelColor sets the color of the labels. func (f *Form) SetLabelColor(color tcell.Color) { f.Lock() defer f.Unlock() f.labelColor = color } // SetLabelColorFocused sets the color of the labels when focused. func (f *Form) SetLabelColorFocused(color tcell.Color) { f.Lock() defer f.Unlock() f.labelColorFocused = color } // SetFieldBackgroundColor sets the background color of the input areas. func (f *Form) SetFieldBackgroundColor(color tcell.Color) { f.Lock() defer f.Unlock() f.fieldBackgroundColor = color } // SetFieldBackgroundColorFocused sets the background color of the input areas when focused. func (f *Form) SetFieldBackgroundColorFocused(color tcell.Color) { f.Lock() defer f.Unlock() f.fieldBackgroundColorFocused = color } // SetFieldTextColor sets the text color of the input areas. func (f *Form) SetFieldTextColor(color tcell.Color) { f.Lock() defer f.Unlock() f.fieldTextColor = color } // SetFieldTextColorFocused sets the text color of the input areas when focused. func (f *Form) SetFieldTextColorFocused(color tcell.Color) { f.Lock() defer f.Unlock() f.fieldTextColorFocused = color } // SetButtonsAlign sets how the buttons align horizontally, one of AlignLeft // (the default), AlignCenter, and AlignRight. This is only func (f *Form) SetButtonsAlign(align int) { f.Lock() defer f.Unlock() f.buttonsAlign = align } // SetButtonBackgroundColor sets the background color of the buttons. func (f *Form) SetButtonBackgroundColor(color tcell.Color) { f.Lock() defer f.Unlock() f.buttonBackgroundColor = color } // SetButtonBackgroundColorFocused sets the background color of the buttons when focused. func (f *Form) SetButtonBackgroundColorFocused(color tcell.Color) { f.Lock() defer f.Unlock() f.buttonBackgroundColorFocused = color } // SetButtonTextColor sets the color of the button texts. func (f *Form) SetButtonTextColor(color tcell.Color) { f.Lock() defer f.Unlock() f.buttonTextColor = color } // SetButtonTextColorFocused sets the color of the button texts when focused. func (f *Form) SetButtonTextColorFocused(color tcell.Color) { f.Lock() defer f.Unlock() f.buttonTextColorFocused = color } // SetFocus shifts the focus to the form element with the given index, counting // non-button items first and buttons last. Note that this index is only used // when the form itself receives focus. func (f *Form) SetFocus(index int) { f.Lock() defer f.Unlock() if index < 0 { f.focusedElement = 0 } else if index >= len(f.items)+len(f.buttons) { f.focusedElement = len(f.items) + len(f.buttons) } else { f.focusedElement = index } } // AddInputField adds an input field to the form. It has a label, an optional // initial value, a field width (a value of 0 extends it as far as possible), // an optional accept function to validate the item's value (set to nil to // accept any text), and an (optional) callback function which is invoked when // the input field's text has changed. func (f *Form) AddInputField(label, value string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) { f.Lock() defer f.Unlock() inputField := NewInputField() inputField.SetLabel(label) inputField.SetText(value) inputField.SetFieldWidth(fieldWidth) inputField.SetAcceptanceFunc(accept) inputField.SetChangedFunc(changed) f.items = append(f.items, inputField) } // AddPasswordField adds a password field to the form. This is similar to an // input field except that the user's input not shown. Instead, a "mask" // character is displayed. The password field has a label, an optional initial // value, a field width (a value of 0 extends it as far as possible), and an // (optional) callback function which is invoked when the input field's text has // changed. func (f *Form) AddPasswordField(label, value string, fieldWidth int, mask rune, changed func(text string)) { f.Lock() defer f.Unlock() if mask == 0 { mask = '*' } passwordField := NewInputField() passwordField.SetLabel(label) passwordField.SetText(value) passwordField.SetFieldWidth(fieldWidth) passwordField.SetMaskCharacter(mask) passwordField.SetChangedFunc(changed) f.items = append(f.items, passwordField) } // AddDropDownSimple adds a drop-down element to the form. It has a label, options, // and an (optional) callback function which is invoked when an option was // selected. The initial option may be a negative value to indicate that no // option is currently selected. func (f *Form) AddDropDownSimple(label string, initialOption int, selected func(index int, option *DropDownOption), options ...string) { f.Lock() defer f.Unlock() dd := NewDropDown() dd.SetLabel(label) dd.SetOptionsSimple(selected, options...) dd.SetCurrentOption(initialOption) f.items = append(f.items, dd) } // AddDropDown adds a drop-down element to the form. It has a label, options, // and an (optional) callback function which is invoked when an option was // selected. The initial option may be a negative value to indicate that no // option is currently selected. func (f *Form) AddDropDown(label string, initialOption int, selected func(index int, option *DropDownOption), options []*DropDownOption) { f.Lock() defer f.Unlock() dd := NewDropDown() dd.SetLabel(label) dd.SetOptions(selected, options...) dd.SetCurrentOption(initialOption) f.items = append(f.items, dd) } // AddCheckBox adds a checkbox to the form. It has a label, a message, an // initial state, and an (optional) callback function which is invoked when the // state of the checkbox was changed by the user. func (f *Form) AddCheckBox(label string, message string, checked bool, changed func(checked bool)) { f.Lock() defer f.Unlock() c := NewCheckBox() c.SetLabel(label) c.SetMessage(message) c.SetChecked(checked) c.SetChangedFunc(changed) f.items = append(f.items, c) } // AddSlider adds a slider to the form. It has a label, an initial value, a // maximum value, an amount to increment by when modified via keyboard, and an // (optional) callback function which is invoked when the state of the slider // was changed by the user. func (f *Form) AddSlider(label string, current, max, increment int, changed func(value int)) { f.Lock() defer f.Unlock() s := NewSlider() s.SetLabel(label) s.SetMax(max) s.SetProgress(current) s.SetIncrement(increment) s.SetChangedFunc(changed) f.items = append(f.items, s) } // AddButton adds a new button to the form. The "selected" function is called // when the user selects this button. It may be nil. func (f *Form) AddButton(label string, selected func()) { f.Lock() defer f.Unlock() button := NewButton(label) button.SetSelectedFunc(selected) f.buttons = append(f.buttons, button) } // GetButton returns the button at the specified 0-based index. Note that // buttons have been specially prepared for this form and modifying some of // their attributes may have unintended side effects. func (f *Form) GetButton(index int) *Button { f.RLock() defer f.RUnlock() return f.buttons[index] } // RemoveButton removes the button at the specified position, starting with 0 // for the button that was added first. func (f *Form) RemoveButton(index int) { f.Lock() defer f.Unlock() f.buttons = append(f.buttons[:index], f.buttons[index+1:]...) } // GetButtonCount returns the number of buttons in this form. func (f *Form) GetButtonCount() int { f.RLock() defer f.RUnlock() return len(f.buttons) } // GetButtonIndex returns the index of the button with the given label, starting // with 0 for the button that was added first. If no such label was found, -1 // is returned. func (f *Form) GetButtonIndex(label string) int { f.RLock() defer f.RUnlock() for index, button := range f.buttons { if button.GetLabel() == label { return index } } return -1 } // Clear removes all input elements from the form, including the buttons if // specified. func (f *Form) Clear(includeButtons bool) { f.Lock() defer f.Unlock() f.items = nil if includeButtons { f.buttons = nil } f.focusedElement = 0 } // ClearButtons removes all buttons from the form. func (f *Form) ClearButtons() { f.Lock() defer f.Unlock() f.buttons = nil } // AddFormItem adds a new item to the form. This can be used to add your own // objects to the form. Note, however, that the Form class will override some // of its attributes to make it work in the form context. Specifically, these // are: // // - The label width // - The label color // - The background color // - The field text color // - The field background color func (f *Form) AddFormItem(item FormItem) { f.Lock() defer f.Unlock() if reflect.ValueOf(item).IsNil() { panic("Invalid FormItem") } f.items = append(f.items, item) } // GetFormItemCount returns the number of items in the form (not including the // buttons). func (f *Form) GetFormItemCount() int { f.RLock() defer f.RUnlock() return len(f.items) } // IndexOfFormItem returns the index of the given FormItem. func (f *Form) IndexOfFormItem(item FormItem) int { f.l.RLock() defer f.l.RUnlock() for index, formItem := range f.items { if item == formItem { return index } } return -1 } // GetFormItem returns the form item at the given position, starting with index // 0. Elements are referenced in the order they were added. Buttons are not included. // If index is out of bounds it returns nil. func (f *Form) GetFormItem(index int) FormItem { f.RLock() defer f.RUnlock() if index > len(f.items)-1 || index < 0 { return nil } return f.items[index] } // RemoveFormItem removes the form element at the given position, starting with // index 0. Elements are referenced in the order they were added. Buttons are // not included. func (f *Form) RemoveFormItem(index int) { f.Lock() defer f.Unlock() f.items = append(f.items[:index], f.items[index+1:]...) } // GetFormItemByLabel returns the first form element with the given label. If // no such element is found, nil is returned. Buttons are not searched and will // therefore not be returned. func (f *Form) GetFormItemByLabel(label string) FormItem { f.RLock() defer f.RUnlock() for _, item := range f.items { if item.GetLabel() == label { return item } } return nil } // GetFormItemIndex returns the index of the first form element with the given // label. If no such element is found, -1 is returned. Buttons are not searched // and will therefore not be returned. func (f *Form) GetFormItemIndex(label string) int { f.RLock() defer f.RUnlock() for index, item := range f.items { if item.GetLabel() == label { return index } } return -1 } // GetFocusedItemIndex returns the indices of the form element or button which // currently has focus. If they don't, -1 is returned resepectively. func (f *Form) GetFocusedItemIndex() (formItem, button int) { f.RLock() defer f.RUnlock() index := f.focusIndex() if index < 0 { return -1, -1 } if index < len(f.items) { return index, -1 } return -1, index - len(f.items) } // SetWrapAround sets the flag that determines whether navigating the form will // wrap around. That is, navigating downwards on the last item will move the // selection to the first item (similarly in the other direction). If set to // false, the selection won't change when navigating downwards on the last item // or navigating upwards on the first item. func (f *Form) SetWrapAround(wrapAround bool) { f.Lock() defer f.Unlock() f.wrapAround = wrapAround } // SetCancelFunc sets a handler which is called when the user hits the Escape // key. func (f *Form) SetCancelFunc(callback func()) { f.Lock() defer f.Unlock() f.cancel = callback } // GetAttributes returns the current attribute settings of a form. func (f *Form) GetAttributes() *FormItemAttributes { f.Lock() defer f.Unlock() return f.getAttributes() } func (f *Form) getAttributes() *FormItemAttributes { attrs := &FormItemAttributes{ BackgroundColor: f.backgroundColor, LabelColor: f.labelColor, FieldBackgroundColor: f.fieldBackgroundColor, FieldTextColor: f.fieldTextColor, } if f.labelColorFocused == ColorUnset { attrs.LabelColorFocused = f.labelColor } else { attrs.LabelColorFocused = f.labelColorFocused } if f.fieldBackgroundColorFocused == ColorUnset { attrs.FieldBackgroundColorFocused = f.fieldTextColor } else { attrs.FieldBackgroundColorFocused = f.fieldBackgroundColorFocused } if f.fieldTextColorFocused == ColorUnset { attrs.FieldTextColorFocused = f.fieldBackgroundColor } else { attrs.FieldTextColorFocused = f.fieldTextColorFocused } return attrs } // Draw draws this primitive onto the screen. func (f *Form) Draw(screen tcell.Screen) { if !f.GetVisible() { return } f.Box.Draw(screen) f.Lock() defer f.Unlock() // Determine the actual item that has focus. if index := f.focusIndex(); index >= 0 { f.focusedElement = index } // Determine the dimensions. x, y, width, height := f.GetInnerRect() topLimit := y bottomLimit := y + height rightLimit := x + width startX := x // Find the longest label. var maxLabelWidth int for _, item := range f.items { labelWidth := TaggedStringWidth(item.GetLabel()) if labelWidth > maxLabelWidth { maxLabelWidth = labelWidth } } maxLabelWidth++ // Add one space. // Calculate positions of form items. positions := make([]struct{ x, y, width, height int }, len(f.items)+len(f.buttons)) var focusedPosition struct{ x, y, width, height int } for index, item := range f.items { if !item.GetVisible() { continue } // Calculate the space needed. labelWidth := TaggedStringWidth(item.GetLabel()) var itemWidth int if f.horizontal { fieldWidth := item.GetFieldWidth() if fieldWidth == 0 { fieldWidth = DefaultFormFieldWidth } labelWidth++ itemWidth = labelWidth + fieldWidth } else { // We want all fields to align vertically. labelWidth = maxLabelWidth itemWidth = width } // Advance to next line if there is no space. if f.horizontal && x+labelWidth+1 >= rightLimit { x = startX y += 2 } // Adjust the item's attributes. if x+itemWidth >= rightLimit { itemWidth = rightLimit - x } attributes := f.getAttributes() attributes.LabelWidth = labelWidth setFormItemAttributes(item, attributes) // Save position. positions[index].x = x positions[index].y = y positions[index].width = itemWidth positions[index].height = 1 if item.GetFocusable().HasFocus() { focusedPosition = positions[index] } // Advance to next item. if f.horizontal { x += itemWidth + f.itemPadding } else { y += item.GetFieldHeight() + f.itemPadding } } // How wide are the buttons? buttonWidths := make([]int, len(f.buttons)) buttonsWidth := 0 for index, button := range f.buttons { w := TaggedStringWidth(button.GetLabel()) + 4 buttonWidths[index] = w buttonsWidth += w + 1 } buttonsWidth-- // Where do we place them? if !f.horizontal && x+buttonsWidth < rightLimit { if f.buttonsAlign == AlignRight { x = rightLimit - buttonsWidth } else if f.buttonsAlign == AlignCenter { x = (x + rightLimit - buttonsWidth) / 2 } // In vertical layouts, buttons always appear after an empty line. if f.itemPadding == 0 { y++ } } // Calculate positions of buttons. for index, button := range f.buttons { if !button.GetVisible() { continue } space := rightLimit - x buttonWidth := buttonWidths[index] if f.horizontal { if space < buttonWidth-4 { x = startX y += 2 space = width } } else { if space < 1 { break // No space for this button anymore. } } if buttonWidth > space { buttonWidth = space } button.SetLabelColor(f.buttonTextColor) button.SetLabelColorFocused(f.buttonTextColorFocused) button.SetBackgroundColorFocused(f.buttonBackgroundColorFocused) button.SetBackgroundColor(f.buttonBackgroundColor) buttonIndex := index + len(f.items) positions[buttonIndex].x = x positions[buttonIndex].y = y positions[buttonIndex].width = buttonWidth positions[buttonIndex].height = 1 if button.HasFocus() { focusedPosition = positions[buttonIndex] } x += buttonWidth + 1 } // Determine vertical offset based on the position of the focused item. var offset int if focusedPosition.y+focusedPosition.height > bottomLimit { offset = focusedPosition.y + focusedPosition.height - bottomLimit if focusedPosition.y-offset < topLimit { offset = focusedPosition.y - topLimit } } // Draw items. for index, item := range f.items { if !item.GetVisible() { continue } // Set position. y := positions[index].y - offset height := positions[index].height item.SetRect(positions[index].x, y, positions[index].width, height) // Is this item visible? if y+height <= topLimit || y >= bottomLimit { continue } // Draw items with focus last (in case of overlaps). if item.GetFocusable().HasFocus() { defer item.Draw(screen) } else { item.Draw(screen) } } // Draw buttons. for index, button := range f.buttons { if !button.GetVisible() { continue } // Set position. buttonIndex := index + len(f.items) y := positions[buttonIndex].y - offset height := positions[buttonIndex].height button.SetRect(positions[buttonIndex].x, y, positions[buttonIndex].width, height) // Is this button visible? if y+height <= topLimit || y >= bottomLimit { continue } // Draw button. button.Draw(screen) } } func (f *Form) updateFocusedElement(decreasing bool) { li := len(f.items) l := len(f.items) + len(f.buttons) for i := 0; i < l; i++ { if f.focusedElement < 0 { if f.wrapAround { f.focusedElement = l - 1 } else { f.focusedElement = 0 } } else if f.focusedElement >= l { if f.wrapAround { f.focusedElement = 0 } else { f.focusedElement = l - 1 } } if f.focusedElement < li { item := f.items[f.focusedElement] if item.GetVisible() { break } } else { button := f.buttons[f.focusedElement-li] if button.GetVisible() { break } } if decreasing { f.focusedElement-- } else { f.focusedElement++ } } } func (f *Form) formItemInputHandler(delegate func(p Primitive)) func(key tcell.Key) { return func(key tcell.Key) { f.Lock() switch key { case tcell.KeyTab, tcell.KeyEnter: f.focusedElement++ f.updateFocusedElement(false) f.Unlock() f.Focus(delegate) f.Lock() case tcell.KeyBacktab: f.focusedElement-- f.updateFocusedElement(true) f.Unlock() f.Focus(delegate) f.Lock() case tcell.KeyEscape: if f.cancel != nil { f.Unlock() f.cancel() f.Lock() } else { f.focusedElement = 0 f.updateFocusedElement(true) f.Unlock() f.Focus(delegate) f.Lock() } } f.Unlock() } } // Focus is called by the application when the primitive receives focus. func (f *Form) Focus(delegate func(p Primitive)) { f.Lock() if len(f.items)+len(f.buttons) == 0 { f.hasFocus = true f.Unlock() return } f.hasFocus = false // Hand on the focus to one of our child elements. if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) { f.focusedElement = 0 } if f.focusedElement < len(f.items) { // We're selecting an item. item := f.items[f.focusedElement] attributes := f.getAttributes() attributes.FinishedFunc = f.formItemInputHandler(delegate) f.Unlock() setFormItemAttributes(item, attributes) delegate(item) } else { // We're selecting a button. button := f.buttons[f.focusedElement-len(f.items)] button.SetBlurFunc(f.formItemInputHandler(delegate)) f.Unlock() delegate(button) } } // HasFocus returns whether or not this primitive has focus. func (f *Form) HasFocus() bool { f.Lock() defer f.Unlock() if f.hasFocus { return true } return f.focusIndex() >= 0 } // focusIndex returns the index of the currently focused item, counting form // items first, then buttons. A negative value indicates that no containeed item // has focus. func (f *Form) focusIndex() int { for index, item := range f.items { if item.GetVisible() && item.GetFocusable().HasFocus() { return index } } for index, button := range f.buttons { if button.GetVisible() && button.focus.HasFocus() { return len(f.items) + index } } return -1 } // MouseHandler returns the mouse handler for this primitive. func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !f.InRect(event.Position()) { return false, nil } // Determine items to pass mouse events to. for _, item := range f.items { consumed, capture = item.MouseHandler()(action, event, setFocus) if consumed { return } } for _, button := range f.buttons { consumed, capture = button.MouseHandler()(action, event, setFocus) if consumed { return } } // A mouse click anywhere else will return the focus to the last selected // element. if action == MouseLeftClick { if f.focusedElement < len(f.items) { setFocus(f.items[f.focusedElement]) } else if f.focusedElement < len(f.items)+len(f.buttons) { setFocus(f.buttons[f.focusedElement-len(f.items)]) } consumed = true } return }) } func setFormItemAttributes(item FormItem, attrs *FormItemAttributes) { item.SetLabelWidth(attrs.LabelWidth) item.SetBackgroundColor(attrs.BackgroundColor) item.SetLabelColor(attrs.LabelColor) item.SetLabelColorFocused(attrs.LabelColorFocused) item.SetFieldTextColor(attrs.FieldTextColor) item.SetFieldTextColorFocused(attrs.FieldTextColorFocused) item.SetFieldBackgroundColor(attrs.FieldBackgroundColor) item.SetFieldBackgroundColorFocused(attrs.FieldBackgroundColorFocused) if attrs.FinishedFunc != nil { item.SetFinishedFunc(attrs.FinishedFunc) } }
package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // frameText holds information about a line of text shown in the frame. type frameText struct { Text string // The text to be displayed. Header bool // true = place in header, false = place in footer. Align int // One of the Align constants. Color tcell.Color // The text color. } // Frame is a wrapper which adds space around another primitive. In addition, // the top area (header) and the bottom area (footer) may also contain text. type Frame struct { *Box // The contained primitive. primitive Primitive // The lines of text to be displayed. text []*frameText // Border spacing. top, bottom, header, footer, left, right int sync.RWMutex } // NewFrame returns a new frame around the given primitive. The primitive's // size will be changed to fit within this frame. func NewFrame(primitive Primitive) *Frame { box := NewBox() f := &Frame{ Box: box, primitive: primitive, top: 1, bottom: 1, header: 1, footer: 1, left: 1, right: 1, } f.focus = f return f } // AddText adds text to the frame. Set "header" to true if the text is to appear // in the header, above the contained primitive. Set it to false for it to // appear in the footer, below the contained primitive. "align" must be one of // the Align constants. Rows in the header are printed top to bottom, rows in // the footer are printed bottom to top. Note that long text can overlap as // different alignments will be placed on the same row. func (f *Frame) AddText(text string, header bool, align int, color tcell.Color) { f.Lock() defer f.Unlock() f.text = append(f.text, &frameText{ Text: text, Header: header, Align: align, Color: color, }) } // Clear removes all text from the frame. func (f *Frame) Clear() { f.Lock() defer f.Unlock() f.text = nil } // SetBorders sets the width of the frame borders as well as "header" and // "footer", the vertical space between the header and footer text and the // contained primitive (does not apply if there is no text). func (f *Frame) SetBorders(top, bottom, header, footer, left, right int) { f.Lock() defer f.Unlock() f.top, f.bottom, f.header, f.footer, f.left, f.right = top, bottom, header, footer, left, right } // Draw draws this primitive onto the screen. func (f *Frame) Draw(screen tcell.Screen) { if !f.GetVisible() { return } f.Box.Draw(screen) f.Lock() defer f.Unlock() // Calculate start positions. x, top, width, height := f.GetInnerRect() bottom := top + height - 1 x += f.left top += f.top bottom -= f.bottom width -= f.left + f.right if width <= 0 || top >= bottom { return // No space left. } // Draw text. var rows [6]int // top-left, top-center, top-right, bottom-left, bottom-center, bottom-right. topMax := top bottomMin := bottom for _, text := range f.text { // Where do we place this text? var y int if text.Header { y = top + rows[text.Align] rows[text.Align]++ if y >= bottomMin { continue } if y+1 > topMax { topMax = y + 1 } } else { y = bottom - rows[3+text.Align] rows[3+text.Align]++ if y <= topMax { continue } if y-1 < bottomMin { bottomMin = y - 1 } } // Draw text. Print(screen, []byte(text.Text), x, y, width, text.Align, text.Color) } // Set the size of the contained primitive. if topMax > top { top = topMax + f.header } if bottomMin < bottom { bottom = bottomMin - f.footer } if top > bottom { return // No space for the primitive. } f.primitive.SetRect(x, top, width, bottom+1-top) // Finally, draw the contained primitive. f.primitive.Draw(screen) } // Focus is called when this primitive receives focus. func (f *Frame) Focus(delegate func(p Primitive)) { f.Lock() primitive := f.primitive defer f.Unlock() delegate(primitive) } // HasFocus returns whether or not this primitive has focus. func (f *Frame) HasFocus() bool { f.RLock() defer f.RUnlock() focusable, ok := f.primitive.(Focusable) if ok { return focusable.HasFocus() } return false } // MouseHandler returns the mouse handler for this primitive. func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !f.InRect(event.Position()) { return false, nil } // Pass mouse events on to contained primitive. return f.primitive.MouseHandler()(action, event, setFocus) }) }
package cview import ( "math" "sync" "github.com/gdamore/tcell/v2" ) // gridItem represents one primitive and its possible position on a grid. type gridItem struct { Item Primitive // The item to be positioned. May be nil for an empty item. Row, Column int // The top-left grid cell where the item is placed. Width, Height int // The number of rows and columns the item occupies. MinGridWidth, MinGridHeight int // The minimum grid width/height for which this item is visible. Focus bool // Whether or not this item attracts the layout's focus. visible bool // Whether or not this item was visible the last time the grid was drawn. x, y, w, h int // The last position of the item relative to the top-left corner of the grid. Undefined if visible is false. } // Grid is an implementation of a grid-based layout. It works by defining the // size of the rows and columns, then placing primitives into the grid. // // Some settings can lead to the grid exceeding its available space. SetOffset() // can then be used to scroll in steps of rows and columns. These offset values // can also be controlled with the arrow keys (or the "g","G", "j", "k", "h", // and "l" keys) while the grid has focus and none of its contained primitives // do. type Grid struct { *Box // The items to be positioned. items []*gridItem // The definition of the rows and columns of the grid. See // SetRows()/SetColumns() for details. rows, columns []int // The minimum sizes for rows and columns. minWidth, minHeight int // The size of the gaps between neighboring primitives. This is automatically // set to 1 if borders is true. gapRows, gapColumns int // The number of rows and columns skipped before drawing the top-left corner // of the grid. rowOffset, columnOffset int // Whether or not borders are drawn around grid items. If this is set to true, // a gap size of 1 is automatically assumed (which is filled with the border // graphics). borders bool // The color of the borders around grid items. bordersColor tcell.Color sync.RWMutex } // NewGrid returns a new grid-based layout container with no initial primitives. // // Note that Grid will have a transparent background by default so that any // areas not covered by any primitives will show primitives behind the Grid. // To disable this transparency: // // grid.SetBackgroundTransparent(false) func NewGrid() *Grid { g := &Grid{ Box: NewBox(), bordersColor: Styles.GraphicsColor, } g.SetBackgroundTransparent(true) g.focus = g return g } // SetColumns defines how the columns of the grid are distributed. Each value // defines the size of one column, starting with the leftmost column. Values // greater 0 represent absolute column widths (gaps not included). Values less // or equal 0 represent proportional column widths or fractions of the remaining // free space, where 0 is treated the same as -1. That is, a column with a value // of -3 will have three times the width of a column with a value of -1 (or 0). // The minimum width set with SetMinSize() is always observed. // // Primitives may extend beyond the columns defined explicitly with this // function. A value of 0 is assumed for any undefined column. In fact, if you // never call this function, all columns occupied by primitives will have the // same width. On the other hand, unoccupied columns defined with this function // will always take their place. // // Assuming a total width of the grid of 100 cells and a minimum width of 0, the // following call will result in columns with widths of 30, 10, 15, 15, and 30 // cells: // // grid.SetColumns(30, 10, -1, -1, -2) // // If a primitive were then placed in the 6th and 7th column, the resulting // widths would be: 30, 10, 10, 10, 20, 10, and 10 cells. // // If you then called SetMinSize() as follows: // // grid.SetMinSize(15, 20) // // The resulting widths would be: 30, 15, 15, 15, 20, 15, and 15 cells, a total // of 125 cells, 25 cells wider than the available grid width. func (g *Grid) SetColumns(columns ...int) { g.Lock() defer g.Unlock() g.columns = columns } // SetRows defines how the rows of the grid are distributed. These values behave // the same as the column values provided with SetColumns(), see there for a // definition and examples. // // The provided values correspond to row heights, the first value defining // the height of the topmost row. func (g *Grid) SetRows(rows ...int) { g.Lock() defer g.Unlock() g.rows = rows } // SetSize is a shortcut for SetRows() and SetColumns() where all row and column // values are set to the given size values. See SetColumns() for details on sizes. func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) { g.Lock() defer g.Unlock() g.rows = make([]int, numRows) for index := range g.rows { g.rows[index] = rowSize } g.columns = make([]int, numColumns) for index := range g.columns { g.columns[index] = columnSize } } // SetMinSize sets an absolute minimum width for rows and an absolute minimum // height for columns. Panics if negative values are provided. func (g *Grid) SetMinSize(row, column int) { g.Lock() defer g.Unlock() if row < 0 || column < 0 { panic("Invalid minimum row/column size") } g.minHeight, g.minWidth = row, column } // SetGap sets the size of the gaps between neighboring primitives on the grid. // If borders are drawn (see SetBorders()), these values are ignored and a gap // of 1 is assumed. Panics if negative values are provided. func (g *Grid) SetGap(row, column int) { g.Lock() defer g.Unlock() if row < 0 || column < 0 { panic("Invalid gap size") } g.gapRows, g.gapColumns = row, column } // SetBorders sets whether or not borders are drawn around grid items. Setting // this value to true will cause the gap values (see SetGap()) to be ignored and // automatically assumed to be 1 where the border graphics are drawn. func (g *Grid) SetBorders(borders bool) { g.Lock() defer g.Unlock() g.borders = borders } // SetBordersColor sets the color of the item borders. func (g *Grid) SetBordersColor(color tcell.Color) { g.Lock() defer g.Unlock() g.bordersColor = color } // AddItem adds a primitive and its position to the grid. The top-left corner // of the primitive will be located in the top-left corner of the grid cell at // the given row and column and will span "rowSpan" rows and "colSpan" columns. // For example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6: // // grid.AddItem(p, 2, 5, 3, 2, 0, 0, true) // // If rowSpan or colSpan is 0, the primitive will not be drawn. // // You can add the same primitive multiple times with different grid positions. // The minGridWidth and minGridHeight values will then determine which of those // positions will be used. This is similar to CSS media queries. These minimum // values refer to the overall size of the grid. If multiple items for the same // primitive apply, the one that has at least one highest minimum value will be // used, or the primitive added last if those values are the same. Example: // // grid.AddItem(p, 0, 0, 0, 0, 0, 0, true). // Hide in small grids. // AddItem(p, 0, 0, 1, 2, 100, 0, true). // One-column layout for medium grids. // AddItem(p, 1, 1, 3, 2, 300, 0, true) // Multi-column layout for large grids. // // To use the same grid layout for all sizes, simply set minGridWidth and // minGridHeight to 0. // // If the item's focus is set to true, it will receive focus when the grid // receives focus. If there are multiple items with a true focus flag, the last // visible one that was added will receive focus. func (g *Grid) AddItem(p Primitive, row, column, rowSpan, colSpan, minGridHeight, minGridWidth int, focus bool) { g.Lock() defer g.Unlock() g.items = append(g.items, &gridItem{ Item: p, Row: row, Column: column, Height: rowSpan, Width: colSpan, MinGridHeight: minGridHeight, MinGridWidth: minGridWidth, Focus: focus, }) } // RemoveItem removes all items for the given primitive from the grid, keeping // the order of the remaining items intact. func (g *Grid) RemoveItem(p Primitive) { g.Lock() defer g.Unlock() for index := len(g.items) - 1; index >= 0; index-- { if g.items[index].Item == p { g.items = append(g.items[:index], g.items[index+1:]...) } } } // Clear removes all items from the grid. func (g *Grid) Clear() { g.Lock() defer g.Unlock() g.items = nil } // SetOffset sets the number of rows and columns which are skipped before // drawing the first grid cell in the top-left corner. As the grid will never // completely move off the screen, these values may be adjusted the next time // the grid is drawn. The actual position of the grid may also be adjusted such // that contained primitives that have focus remain visible. func (g *Grid) SetOffset(rows, columns int) { g.Lock() defer g.Unlock() g.rowOffset, g.columnOffset = rows, columns } // GetOffset returns the current row and column offset (see SetOffset() for // details). func (g *Grid) GetOffset() (rows, columns int) { g.RLock() defer g.RUnlock() return g.rowOffset, g.columnOffset } // Focus is called when this primitive receives focus. func (g *Grid) Focus(delegate func(p Primitive)) { g.Lock() items := g.items g.Unlock() for _, item := range items { if item.Focus { delegate(item.Item) return } } g.Lock() g.hasFocus = true g.Unlock() } // Blur is called when this primitive loses focus. func (g *Grid) Blur() { g.Lock() defer g.Unlock() g.hasFocus = false } // HasFocus returns whether or not this primitive has focus. func (g *Grid) HasFocus() bool { g.RLock() defer g.RUnlock() for _, item := range g.items { if item.visible && item.Item.GetFocusable().HasFocus() { return true } } return g.hasFocus } // InputHandler returns the handler for this primitive. func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { g.Lock() defer g.Unlock() if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) { g.rowOffset, g.columnOffset = 0, 0 } else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) { g.rowOffset = math.MaxInt32 } else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MovePreviousField) { g.rowOffset-- } else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) { g.rowOffset++ } else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) { g.columnOffset-- } else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) { g.columnOffset++ } }) } // Draw draws this primitive onto the screen. func (g *Grid) Draw(screen tcell.Screen) { if !g.GetVisible() { return } g.Box.Draw(screen) g.Lock() defer g.Unlock() x, y, width, height := g.GetInnerRect() screenWidth, screenHeight := screen.Size() // Make a list of items which apply. items := make(map[Primitive]*gridItem) for _, item := range g.items { item.visible = false if item.Width <= 0 || item.Height <= 0 || width < item.MinGridWidth || height < item.MinGridHeight { continue } previousItem, ok := items[item.Item] if ok && item.MinGridWidth < previousItem.MinGridWidth && item.MinGridHeight < previousItem.MinGridHeight { continue } items[item.Item] = item } // How many rows and columns do we have? rows := len(g.rows) columns := len(g.columns) for _, item := range items { rowEnd := item.Row + item.Height if rowEnd > rows { rows = rowEnd } columnEnd := item.Column + item.Width if columnEnd > columns { columns = columnEnd } } if rows == 0 || columns == 0 { return // No content. } // Where are they located? rowPos := make([]int, rows) rowHeight := make([]int, rows) columnPos := make([]int, columns) columnWidth := make([]int, columns) // How much space do we distribute? remainingWidth := width remainingHeight := height proportionalWidth := 0 proportionalHeight := 0 for index, row := range g.rows { if row > 0 { if row < g.minHeight { row = g.minHeight } remainingHeight -= row rowHeight[index] = row } else if row == 0 { proportionalHeight++ } else { proportionalHeight += -row } } for index, column := range g.columns { if column > 0 { if column < g.minWidth { column = g.minWidth } remainingWidth -= column columnWidth[index] = column } else if column == 0 { proportionalWidth++ } else { proportionalWidth += -column } } if g.borders { remainingHeight -= rows + 1 remainingWidth -= columns + 1 } else { remainingHeight -= (rows - 1) * g.gapRows remainingWidth -= (columns - 1) * g.gapColumns } if rows > len(g.rows) { proportionalHeight += rows - len(g.rows) } if columns > len(g.columns) { proportionalWidth += columns - len(g.columns) } // Distribute proportional rows/columns. for index := 0; index < rows; index++ { row := 0 if index < len(g.rows) { row = g.rows[index] } if row > 0 { if row < g.minHeight { row = g.minHeight } continue // Not proportional. We already know the width. } else if row == 0 { row = 1 } else { row = -row } rowAbs := row * remainingHeight / proportionalHeight remainingHeight -= rowAbs proportionalHeight -= row if rowAbs < g.minHeight { rowAbs = g.minHeight } rowHeight[index] = rowAbs } for index := 0; index < columns; index++ { column := 0 if index < len(g.columns) { column = g.columns[index] } if column > 0 { if column < g.minWidth { column = g.minWidth } continue // Not proportional. We already know the height. } else if column == 0 { column = 1 } else { column = -column } columnAbs := column * remainingWidth / proportionalWidth remainingWidth -= columnAbs proportionalWidth -= column if columnAbs < g.minWidth { columnAbs = g.minWidth } columnWidth[index] = columnAbs } // Calculate row/column positions. var columnX, rowY int if g.borders { columnX++ rowY++ } for index, row := range rowHeight { rowPos[index] = rowY gap := g.gapRows if g.borders { gap = 1 } rowY += row + gap } for index, column := range columnWidth { columnPos[index] = columnX gap := g.gapColumns if g.borders { gap = 1 } columnX += column + gap } // Calculate primitive positions. var focus *gridItem // The item which has focus. for primitive, item := range items { px := columnPos[item.Column] py := rowPos[item.Row] var pw, ph int for index := 0; index < item.Height; index++ { ph += rowHeight[item.Row+index] } for index := 0; index < item.Width; index++ { pw += columnWidth[item.Column+index] } if g.borders { pw += item.Width - 1 ph += item.Height - 1 } else { pw += (item.Width - 1) * g.gapColumns ph += (item.Height - 1) * g.gapRows } item.x, item.y, item.w, item.h = px, py, pw, ph item.visible = true if primitive.GetFocusable().HasFocus() { focus = item } } // Calculate screen offsets. var offsetX, offsetY int add := 1 if !g.borders { add = g.gapRows } for index, height := range rowHeight { if index >= g.rowOffset { break } offsetY += height + add } if !g.borders { add = g.gapColumns } for index, width := range columnWidth { if index >= g.columnOffset { break } offsetX += width + add } // Line up the last row/column with the end of the available area. var border int if g.borders { border = 1 } last := len(rowPos) - 1 if rowPos[last]+rowHeight[last]+border-offsetY < height { offsetY = rowPos[last] - height + rowHeight[last] + border } last = len(columnPos) - 1 if columnPos[last]+columnWidth[last]+border-offsetX < width { offsetX = columnPos[last] - width + columnWidth[last] + border } // The focused item must be within the visible area. if focus != nil { if focus.y+focus.h-offsetY >= height { offsetY = focus.y - height + focus.h } if focus.y-offsetY < 0 { offsetY = focus.y } if focus.x+focus.w-offsetX >= width { offsetX = focus.x - width + focus.w } if focus.x-offsetX < 0 { offsetX = focus.x } } // Adjust row/column offsets based on this value. var from, to int for index, pos := range rowPos { if pos-offsetY < 0 { from = index + 1 } if pos-offsetY < height { to = index } } if g.rowOffset < from { g.rowOffset = from } if g.rowOffset > to { g.rowOffset = to } from, to = 0, 0 for index, pos := range columnPos { if pos-offsetX < 0 { from = index + 1 } if pos-offsetX < width { to = index } } if g.columnOffset < from { g.columnOffset = from } if g.columnOffset > to { g.columnOffset = to } // Draw primitives and borders. for primitive, item := range items { // Final primitive position. if !item.visible { continue } item.x -= offsetX item.y -= offsetY if item.x >= width || item.x+item.w <= 0 || item.y >= height || item.y+item.h <= 0 { item.visible = false continue } if item.x+item.w > width { item.w = width - item.x } if item.y+item.h > height { item.h = height - item.y } if item.x < 0 { item.w += item.x item.x = 0 } if item.y < 0 { item.h += item.y item.y = 0 } if item.w <= 0 || item.h <= 0 { item.visible = false continue } item.x += x item.y += y primitive.SetRect(item.x, item.y, item.w, item.h) // Draw primitive. if item == focus { defer primitive.Draw(screen) } else { primitive.Draw(screen) } // Draw border around primitive. if g.borders { for bx := item.x; bx < item.x+item.w; bx++ { // Top/bottom lines. if bx < 0 || bx >= screenWidth { continue } by := item.y - 1 if by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, g.bordersColor) } by = item.y + item.h if by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, g.bordersColor) } } for by := item.y; by < item.y+item.h; by++ { // Left/right lines. if by < 0 || by >= screenHeight { continue } bx := item.x - 1 if bx >= 0 && bx < screenWidth { PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, g.bordersColor) } bx = item.x + item.w if bx >= 0 && bx < screenWidth { PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, g.bordersColor) } } bx, by := item.x-1, item.y-1 // Top-left corner. if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.TopLeft, g.bordersColor) } bx, by = item.x+item.w, item.y-1 // Top-right corner. if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.TopRight, g.bordersColor) } bx, by = item.x-1, item.y+item.h // Bottom-left corner. if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.BottomLeft, g.bordersColor) } bx, by = item.x+item.w, item.y+item.h // Bottom-right corner. if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { PrintJoinedSemigraphics(screen, bx, by, Borders.BottomRight, g.bordersColor) } } } } // MouseHandler returns the mouse handler for this primitive. func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return g.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !g.InRect(event.Position()) { return false, nil } // Pass mouse events along to the first child item that takes it. for _, item := range g.items { consumed, capture = item.Item.MouseHandler()(action, event, setFocus) if consumed { return } } return }) }
package cview import ( "bytes" "math" "regexp" "sync" "unicode/utf8" "github.com/gdamore/tcell/v2" "github.com/mattn/go-runewidth" ) // InputField is a one-line box (three lines if there is a title) where the // user can enter text. Use SetAcceptanceFunc() to accept or reject input, // SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input // from onlookers (e.g. for password input). // // The following keys can be used for navigation and editing: // // - Left arrow: Move left by one character. // - Right arrow: Move right by one character. // - Home, Ctrl-A, Alt-a: Move to the beginning of the line. // - End, Ctrl-E, Alt-e: Move to the end of the line. // - Alt-left, Alt-b: Move left by one word. // - Alt-right, Alt-f: Move right by one word. // - Backspace: Delete the character before the cursor. // - Delete: Delete the character after the cursor. // - Ctrl-K: Delete from the cursor to the end of the line. // - Ctrl-W: Delete the last word before the cursor. // - Ctrl-U: Delete the entire line. type InputField struct { *Box // The text that was entered. text []byte // The text to be displayed before the input area. label []byte // The text to be displayed in the input area when "text" is empty. placeholder []byte // The label color. labelColor tcell.Color // The label color when focused. labelColorFocused tcell.Color // The background color of the input area. fieldBackgroundColor tcell.Color // The background color of the input area when focused. fieldBackgroundColorFocused tcell.Color // The text color of the input area. fieldTextColor tcell.Color // The text color of the input area when focused. fieldTextColorFocused tcell.Color // The text color of the placeholder. placeholderTextColor tcell.Color // The text color of the placeholder when focused. placeholderTextColorFocused tcell.Color // The text color of the list items. autocompleteListTextColor tcell.Color // The background color of the autocomplete list. autocompleteListBackgroundColor tcell.Color // The text color of the selected ListItem. autocompleteListSelectedTextColor tcell.Color // The background color of the selected ListItem. autocompleteListSelectedBackgroundColor tcell.Color // The text color of the suggestion. autocompleteSuggestionTextColor tcell.Color // The text color of the note below the input field. fieldNoteTextColor tcell.Color // The note to show below the input field. fieldNote []byte // The screen width of the label area. A value of 0 means use the width of // the label text. labelWidth int // The screen width of the input area. A value of 0 means extend as much as // possible. fieldWidth int // A character to mask entered text (useful for password fields). A value of 0 // disables masking. maskCharacter rune // The cursor position as a byte index into the text string. cursorPos int // An optional autocomplete function which receives the current text of the // input field and returns a slice of ListItems to be displayed in a drop-down // selection. Items' main text is displayed in the autocomplete list. When // set, items' secondary text is used as the selection value. Otherwise, // the main text is used. autocomplete func(text string) []*ListItem // The List object which shows the selectable autocomplete entries. If not // nil, the list's main texts represent the current autocomplete entries. autocompleteList *List // The suggested completion of the current autocomplete ListItem. autocompleteListSuggestion []byte // An optional function which may reject the last character that was entered. accept func(text string, ch rune) bool // An optional function which is called when the input has changed. changed func(text string) // An optional function which is called when the user indicated that they // are done entering text. The key which was pressed is provided (tab, // shift-tab, enter, or escape). done func(tcell.Key) // A callback function set by the Form class and called when the user leaves // this form item. finished func(tcell.Key) // The x-coordinate of the input field as determined during the last call to Draw(). fieldX int // The number of bytes of the text string skipped ahead while drawing. offset int sync.RWMutex } // NewInputField returns a new input field. func NewInputField() *InputField { return &InputField{ Box: NewBox(), labelColor: Styles.SecondaryTextColor, fieldBackgroundColor: Styles.ContrastBackgroundColor, fieldTextColor: Styles.PrimaryTextColor, placeholderTextColor: Styles.ContrastSecondaryTextColor, autocompleteListTextColor: Styles.PrimitiveBackgroundColor, autocompleteListBackgroundColor: Styles.MoreContrastBackgroundColor, autocompleteListSelectedTextColor: Styles.PrimitiveBackgroundColor, autocompleteListSelectedBackgroundColor: Styles.PrimaryTextColor, autocompleteSuggestionTextColor: Styles.ContrastPrimaryTextColor, fieldNoteTextColor: Styles.SecondaryTextColor, labelColorFocused: ColorUnset, fieldBackgroundColorFocused: ColorUnset, fieldTextColorFocused: ColorUnset, placeholderTextColorFocused: ColorUnset, } } // SetText sets the current text of the input field. func (i *InputField) SetText(text string) { i.Lock() i.text = []byte(text) i.cursorPos = len(text) if i.changed != nil { i.Unlock() i.changed(text) } else { i.Unlock() } } // GetText returns the current text of the input field. func (i *InputField) GetText() string { i.RLock() defer i.RUnlock() return string(i.text) } // SetLabel sets the text to be displayed before the input area. func (i *InputField) SetLabel(label string) { i.Lock() defer i.Unlock() i.label = []byte(label) } // GetLabel returns the text to be displayed before the input area. func (i *InputField) GetLabel() string { i.RLock() defer i.RUnlock() return string(i.label) } // SetLabelWidth sets the screen width of the label. A value of 0 will cause the // primitive to use the width of the label string. func (i *InputField) SetLabelWidth(width int) { i.Lock() defer i.Unlock() i.labelWidth = width } // SetPlaceholder sets the text to be displayed when the input text is empty. func (i *InputField) SetPlaceholder(text string) { i.Lock() defer i.Unlock() i.placeholder = []byte(text) } // SetLabelColor sets the color of the label. func (i *InputField) SetLabelColor(color tcell.Color) { i.Lock() defer i.Unlock() i.labelColor = color } // SetLabelColorFocused sets the color of the label when focused. func (i *InputField) SetLabelColorFocused(color tcell.Color) { i.Lock() defer i.Unlock() i.labelColorFocused = color } // SetFieldBackgroundColor sets the background color of the input area. func (i *InputField) SetFieldBackgroundColor(color tcell.Color) { i.Lock() defer i.Unlock() i.fieldBackgroundColor = color } // SetFieldBackgroundColorFocused sets the background color of the input area // when focused. func (i *InputField) SetFieldBackgroundColorFocused(color tcell.Color) { i.Lock() defer i.Unlock() i.fieldBackgroundColorFocused = color } // SetFieldTextColor sets the text color of the input area. func (i *InputField) SetFieldTextColor(color tcell.Color) { i.Lock() defer i.Unlock() i.fieldTextColor = color } // SetFieldTextColorFocused sets the text color of the input area when focused. func (i *InputField) SetFieldTextColorFocused(color tcell.Color) { i.Lock() defer i.Unlock() i.fieldTextColorFocused = color } // SetPlaceholderTextColor sets the text color of placeholder text. func (i *InputField) SetPlaceholderTextColor(color tcell.Color) { i.Lock() defer i.Unlock() i.placeholderTextColor = color } // SetPlaceholderTextColorFocused sets the text color of placeholder text when // focused. func (i *InputField) SetPlaceholderTextColorFocused(color tcell.Color) { i.Lock() defer i.Unlock() i.placeholderTextColorFocused = color } // SetAutocompleteListTextColor sets the text color of the ListItems. func (i *InputField) SetAutocompleteListTextColor(color tcell.Color) { i.Lock() defer i.Unlock() i.autocompleteListTextColor = color } // SetAutocompleteListBackgroundColor sets the background color of the // autocomplete list. func (i *InputField) SetAutocompleteListBackgroundColor(color tcell.Color) { i.Lock() defer i.Unlock() i.autocompleteListBackgroundColor = color } // SetAutocompleteListSelectedTextColor sets the text color of the selected // ListItem. func (i *InputField) SetAutocompleteListSelectedTextColor(color tcell.Color) { i.Lock() defer i.Unlock() i.autocompleteListSelectedTextColor = color } // SetAutocompleteListSelectedBackgroundColor sets the background of the // selected ListItem. func (i *InputField) SetAutocompleteListSelectedBackgroundColor(color tcell.Color) { i.Lock() defer i.Unlock() i.autocompleteListSelectedBackgroundColor = color } // SetAutocompleteSuggestionTextColor sets the text color of the autocomplete // suggestion in the input field. func (i *InputField) SetAutocompleteSuggestionTextColor(color tcell.Color) { i.Lock() defer i.Unlock() i.autocompleteSuggestionTextColor = color } // SetFieldNoteTextColor sets the text color of the note. func (i *InputField) SetFieldNoteTextColor(color tcell.Color) { i.Lock() defer i.Unlock() i.fieldNoteTextColor = color } // SetFieldNote sets the text to show below the input field, e.g. when the // input is invalid. func (i *InputField) SetFieldNote(note string) { i.Lock() defer i.Unlock() i.fieldNote = []byte(note) } // ResetFieldNote sets the note to an empty string. func (i *InputField) ResetFieldNote() { i.Lock() defer i.Unlock() i.fieldNote = nil } // SetFieldWidth sets the screen width of the input area. A value of 0 means // extend as much as possible. func (i *InputField) SetFieldWidth(width int) { i.Lock() defer i.Unlock() i.fieldWidth = width } // GetFieldWidth returns this primitive's field width. func (i *InputField) GetFieldWidth() int { i.RLock() defer i.RUnlock() return i.fieldWidth } // GetFieldHeight returns the height of the field. func (i *InputField) GetFieldHeight() int { i.RLock() defer i.RUnlock() if len(i.fieldNote) == 0 { return 1 } return 2 } // GetCursorPosition returns the cursor position. func (i *InputField) GetCursorPosition() int { i.RLock() defer i.RUnlock() return i.cursorPos } // SetCursorPosition sets the cursor position. func (i *InputField) SetCursorPosition(cursorPos int) { i.Lock() defer i.Unlock() i.cursorPos = cursorPos } // SetMaskCharacter sets a character that masks user input on a screen. A value // of 0 disables masking. func (i *InputField) SetMaskCharacter(mask rune) { i.Lock() defer i.Unlock() i.maskCharacter = mask } // SetAutocompleteFunc sets an autocomplete callback function which may return // ListItems to be selected from a drop-down based on the current text of the // input field. The drop-down appears only if len(entries) > 0. The callback is // invoked in this function and whenever the current text changes or when // Autocomplete() is called. Entries are cleared when the user selects an entry // or presses Escape. func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []*ListItem)) { i.Lock() i.autocomplete = callback i.Unlock() i.Autocomplete() } // Autocomplete invokes the autocomplete callback (if there is one). If the // length of the returned autocomplete entries slice is greater than 0, the // input field will present the user with a corresponding drop-down list the // next time the input field is drawn. // // It is safe to call this function from any goroutine. Note that the input // field is not redrawn automatically unless called from the main goroutine // (e.g. in response to events). func (i *InputField) Autocomplete() { i.Lock() if i.autocomplete == nil { i.Unlock() return } i.Unlock() // Do we have any autocomplete entries? entries := i.autocomplete(string(i.text)) if len(entries) == 0 { // No entries, no list. i.Lock() i.autocompleteList = nil i.autocompleteListSuggestion = nil i.Unlock() return } i.Lock() // Make a list if we have none. if i.autocompleteList == nil { l := NewList() l.SetChangedFunc(i.autocompleteChanged) l.ShowSecondaryText(false) l.SetMainTextColor(i.autocompleteListTextColor) l.SetSelectedTextColor(i.autocompleteListSelectedTextColor) l.SetSelectedBackgroundColor(i.autocompleteListSelectedBackgroundColor) l.SetHighlightFullLine(true) l.SetBackgroundColor(i.autocompleteListBackgroundColor) i.autocompleteList = l } // Fill it with the entries. currentEntry := -1 i.autocompleteList.Clear() for index, entry := range entries { i.autocompleteList.AddItem(entry) if currentEntry < 0 && entry.GetMainText() == string(i.text) { currentEntry = index } } // Set the selection if we have one. if currentEntry >= 0 { i.autocompleteList.SetCurrentItem(currentEntry) } i.Unlock() } // autocompleteChanged gets called when another item in the // autocomplete list has been selected. func (i *InputField) autocompleteChanged(_ int, item *ListItem) { mainText := item.GetMainBytes() secondaryText := item.GetSecondaryBytes() if len(i.text) < len(secondaryText) { i.autocompleteListSuggestion = secondaryText[len(i.text):] } else if len(i.text) < len(mainText) { i.autocompleteListSuggestion = mainText[len(i.text):] } else { i.autocompleteListSuggestion = nil } } // SetAcceptanceFunc sets a handler which may reject the last character that was // entered (by returning false). // // This package defines a number of variables prefixed with InputField which may // be used for common input (e.g. numbers, maximum text length). func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) { i.Lock() defer i.Unlock() i.accept = handler } // SetChangedFunc sets a handler which is called whenever the text of the input // field has changed. It receives the current text (after the change). func (i *InputField) SetChangedFunc(handler func(text string)) { i.Lock() defer i.Unlock() i.changed = handler } // SetDoneFunc sets a handler which is called when the user is done entering // text. The callback function is provided with the key that was pressed, which // is one of the following: // // - KeyEnter: Done entering text. // - KeyEscape: Abort text input. // - KeyTab: Move to the next field. // - KeyBacktab: Move to the previous field. func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) { i.Lock() defer i.Unlock() i.done = handler } // SetFinishedFunc sets a callback invoked when the user leaves this form item. func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) { i.Lock() defer i.Unlock() i.finished = handler } // Draw draws this primitive onto the screen. func (i *InputField) Draw(screen tcell.Screen) { if !i.GetVisible() { return } i.Box.Draw(screen) i.Lock() defer i.Unlock() // Select colors labelColor := i.labelColor fieldBackgroundColor := i.fieldBackgroundColor fieldTextColor := i.fieldTextColor if i.GetFocusable().HasFocus() { if i.labelColorFocused != ColorUnset { labelColor = i.labelColorFocused } if i.fieldBackgroundColorFocused != ColorUnset { fieldBackgroundColor = i.fieldBackgroundColorFocused } if i.fieldTextColorFocused != ColorUnset { fieldTextColor = i.fieldTextColorFocused } } // Prepare x, y, width, height := i.GetInnerRect() rightLimit := x + width if height < 1 || rightLimit <= x { return } // Draw label. if i.labelWidth > 0 { labelWidth := i.labelWidth if labelWidth > rightLimit-x { labelWidth = rightLimit - x } Print(screen, i.label, x, y, labelWidth, AlignLeft, labelColor) x += labelWidth } else { _, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, labelColor) x += drawnWidth } // Draw input area. i.fieldX = x fieldWidth := i.fieldWidth if fieldWidth == 0 { fieldWidth = math.MaxInt32 } if rightLimit-x < fieldWidth { fieldWidth = rightLimit - x } fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor) for index := 0; index < fieldWidth; index++ { screen.SetContent(x+index, y, ' ', nil, fieldStyle) } // Text. var cursorScreenPos int text := i.text if len(text) == 0 && len(i.placeholder) > 0 { // Draw placeholder text. placeholderTextColor := i.placeholderTextColor if i.GetFocusable().HasFocus() && i.placeholderTextColorFocused != ColorUnset { placeholderTextColor = i.placeholderTextColorFocused } Print(screen, EscapeBytes(i.placeholder), x, y, fieldWidth, AlignLeft, placeholderTextColor) i.offset = 0 } else { // Draw entered text. if i.maskCharacter > 0 { text = bytes.Repeat([]byte(string(i.maskCharacter)), utf8.RuneCount(i.text)) } var drawnText []byte if fieldWidth >= runewidth.StringWidth(string(text)) { // We have enough space for the full text. drawnText = EscapeBytes(text) Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor) i.offset = 0 iterateString(string(text), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { if textPos >= i.cursorPos { return true } cursorScreenPos += screenWidth return false }) } else { // The text doesn't fit. Where is the cursor? if i.cursorPos < 0 { i.cursorPos = 0 } else if i.cursorPos > len(text) { i.cursorPos = len(text) } // Shift the text so the cursor is inside the field. var shiftLeft int if i.offset > i.cursorPos { i.offset = i.cursorPos } else if subWidth := runewidth.StringWidth(string(text[i.offset:i.cursorPos])); subWidth > fieldWidth-1 { shiftLeft = subWidth - fieldWidth + 1 } currentOffset := i.offset iterateString(string(text), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { if textPos >= currentOffset { if shiftLeft > 0 { i.offset = textPos + textWidth shiftLeft -= screenWidth } else { if textPos+textWidth > i.cursorPos { return true } cursorScreenPos += screenWidth } } return false }) drawnText = EscapeBytes(text[i.offset:]) Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor) } // Draw suggestion if i.maskCharacter == 0 && len(i.autocompleteListSuggestion) > 0 { Print(screen, i.autocompleteListSuggestion, x+runewidth.StringWidth(string(drawnText)), y, fieldWidth-runewidth.StringWidth(string(drawnText)), AlignLeft, i.autocompleteSuggestionTextColor) } } // Draw field note if len(i.fieldNote) > 0 { Print(screen, i.fieldNote, x, y+1, fieldWidth, AlignLeft, i.fieldNoteTextColor) } // Draw autocomplete list. if i.autocompleteList != nil { // How much space do we need? lheight := i.autocompleteList.GetItemCount() lwidth := 0 for index := 0; index < lheight; index++ { entry, _ := i.autocompleteList.GetItemText(index) width := TaggedStringWidth(entry) if width > lwidth { lwidth = width } } // We prefer to drop down but if there is no space, maybe drop up? lx := x ly := y + 1 _, sheight := screen.Size() if ly+lheight >= sheight && ly-2 > lheight-ly { ly = y - lheight if ly < 0 { ly = 0 } } if ly+lheight >= sheight { lheight = sheight - ly } if i.autocompleteList.scrollBarVisibility == ScrollBarAlways || (i.autocompleteList.scrollBarVisibility == ScrollBarAuto && i.autocompleteList.GetItemCount() > lheight) { lwidth++ // Add space for scroll bar } i.autocompleteList.SetRect(lx, ly, lwidth, lheight) i.autocompleteList.Draw(screen) } // Set cursor. if i.focus.HasFocus() { screen.ShowCursor(x+cursorScreenPos, y) } } // InputHandler returns the handler for this primitive. func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { i.Lock() // Trigger changed events. currentText := i.text defer func() { i.Lock() newText := i.text i.Unlock() if !bytes.Equal(newText, currentText) { i.Autocomplete() if i.changed != nil { i.changed(string(i.text)) } } }() // Movement functions. home := func() { i.cursorPos = 0 } end := func() { i.cursorPos = len(i.text) } moveLeft := func() { iterateStringReverse(string(i.text[:i.cursorPos]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { i.cursorPos -= textWidth return true }) } moveRight := func() { iterateString(string(i.text[i.cursorPos:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { i.cursorPos += textWidth return true }) } moveWordLeft := func() { i.cursorPos = len(regexRightWord.ReplaceAll(i.text[:i.cursorPos], nil)) } moveWordRight := func() { i.cursorPos = len(i.text) - len(regexLeftWord.ReplaceAll(i.text[i.cursorPos:], nil)) } // Add character function. Returns whether or not the rune character is // accepted. add := func(r rune) bool { newText := append(append(i.text[:i.cursorPos], []byte(string(r))...), i.text[i.cursorPos:]...) if i.accept != nil && !i.accept(string(newText), r) { return false } i.text = newText i.cursorPos += len(string(r)) return true } // Finish up. finish := func(key tcell.Key) { if i.done != nil { i.done(key) } if i.finished != nil { i.finished(key) } } // Process key event. switch key := event.Key(); key { case tcell.KeyRune: // Regular character. if event.Modifiers()&tcell.ModAlt > 0 { // We accept some Alt- key combinations. switch event.Rune() { case 'a': // Home. home() case 'e': // End. end() case 'b': // Move word left. moveWordLeft() case 'f': // Move word right. moveWordRight() default: if !add(event.Rune()) { i.Unlock() return } } } else { // Other keys are simply accepted as regular characters. if !add(event.Rune()) { i.Unlock() return } } case tcell.KeyCtrlU: // Delete all. i.text = nil i.cursorPos = 0 case tcell.KeyCtrlK: // Delete until the end of the line. i.text = i.text[:i.cursorPos] case tcell.KeyCtrlW: // Delete last word. newText := append(regexRightWord.ReplaceAll(i.text[:i.cursorPos], nil), i.text[i.cursorPos:]...) i.cursorPos -= len(i.text) - len(newText) i.text = newText case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor. iterateStringReverse(string(i.text[:i.cursorPos]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { i.text = append(i.text[:textPos], i.text[textPos+textWidth:]...) i.cursorPos -= textWidth return true }) if i.offset >= i.cursorPos { i.offset = 0 } case tcell.KeyDelete: // Delete character after the cursor. iterateString(string(i.text[i.cursorPos:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { i.text = append(i.text[:i.cursorPos], i.text[i.cursorPos+textWidth:]...) return true }) case tcell.KeyLeft: if event.Modifiers()&tcell.ModAlt > 0 { moveWordLeft() } else { moveLeft() } case tcell.KeyRight: if event.Modifiers()&tcell.ModAlt > 0 { moveWordRight() } else { moveRight() } case tcell.KeyHome, tcell.KeyCtrlA: home() case tcell.KeyEnd, tcell.KeyCtrlE: end() case tcell.KeyEnter: // We might be done. if i.autocompleteList != nil { currentItem := i.autocompleteList.GetCurrentItem() selectionText := currentItem.GetMainText() if currentItem.GetSecondaryText() != "" { selectionText = currentItem.GetSecondaryText() } i.Unlock() i.SetText(selectionText) i.Lock() i.autocompleteList = nil i.autocompleteListSuggestion = nil i.Unlock() } else { i.Unlock() finish(key) } return case tcell.KeyEscape: if i.autocompleteList != nil { i.autocompleteList = nil i.autocompleteListSuggestion = nil i.Unlock() } else { i.Unlock() finish(key) } return case tcell.KeyDown, tcell.KeyTab: // Autocomplete selection. if i.autocompleteList != nil { count := i.autocompleteList.GetItemCount() newEntry := i.autocompleteList.GetCurrentItemIndex() + 1 if newEntry >= count { newEntry = 0 } i.autocompleteList.SetCurrentItem(newEntry) i.Unlock() } else { i.Unlock() finish(key) } return case tcell.KeyUp, tcell.KeyBacktab: // Autocomplete selection. if i.autocompleteList != nil { newEntry := i.autocompleteList.GetCurrentItemIndex() - 1 if newEntry < 0 { newEntry = i.autocompleteList.GetItemCount() - 1 } i.autocompleteList.SetCurrentItem(newEntry) i.Unlock() } else { i.Unlock() finish(key) } return } i.Unlock() }) } // MouseHandler returns the mouse handler for this primitive. func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { x, y := event.Position() _, rectY, _, _ := i.GetInnerRect() if !i.InRect(x, y) { return false, nil } // Process mouse event. if action == MouseLeftClick && y == rectY { // Determine where to place the cursor. if x >= i.fieldX { if !iterateString(string(i.text), func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool { if x-i.fieldX < screenPos+screenWidth { i.cursorPos = textPos return true } return false }) { i.cursorPos = len(i.text) } } setFocus(i) consumed = true } return }) } var ( regexRightWord = regexp.MustCompile(`(\w*|\W)$`) regexLeftWord = regexp.MustCompile(`^(\W|\w*)`) )
package cview import ( "code.rocketnine.space/tslocum/cbind" "github.com/gdamore/tcell/v2" ) // Key defines the keyboard shortcuts of an application. // Secondary shortcuts apply when not focusing a text input. type Key struct { Cancel []string Select []string Select2 []string MoveUp []string MoveUp2 []string MoveDown []string MoveDown2 []string MoveLeft []string MoveLeft2 []string MoveRight []string MoveRight2 []string MoveFirst []string MoveFirst2 []string MoveLast []string MoveLast2 []string MovePreviousField []string MoveNextField []string MovePreviousPage []string MoveNextPage []string ShowContextMenu []string } // Keys defines the keyboard shortcuts of an application. // Secondary shortcuts apply when not focusing a text input. var Keys = Key{ Cancel: []string{"Escape"}, Select: []string{"Enter", "Ctrl+J"}, // Ctrl+J = keypad enter Select2: []string{"Space"}, MoveUp: []string{"Up"}, MoveUp2: []string{"k"}, MoveDown: []string{"Down"}, MoveDown2: []string{"j"}, MoveLeft: []string{"Left"}, MoveLeft2: []string{"h"}, MoveRight: []string{"Right"}, MoveRight2: []string{"l"}, MoveFirst: []string{"Home", "Ctrl+A"}, MoveFirst2: []string{"g"}, MoveLast: []string{"End", "Ctrl+E"}, MoveLast2: []string{"G"}, MovePreviousField: []string{"Backtab"}, MoveNextField: []string{"Tab"}, MovePreviousPage: []string{"PageUp", "Ctrl+B"}, MoveNextPage: []string{"PageDown", "Ctrl+F"}, ShowContextMenu: []string{"Alt+Enter"}, } // HitShortcut returns whether the EventKey provided is present in one or more // sets of keybindings. func HitShortcut(event *tcell.EventKey, keybindings ...[]string) bool { enc, err := cbind.Encode(event.Modifiers(), event.Key(), event.Rune()) if err != nil { return false } for _, binds := range keybindings { for _, key := range binds { if key == enc { return true } } } return false }
package cview import ( "bytes" "fmt" "strings" "sync" "github.com/gdamore/tcell/v2" ) // ListItem represents an item in a List. type ListItem struct { disabled bool // Whether or not the list item is selectable. mainText []byte // The main text of the list item. secondaryText []byte // A secondary text to be shown underneath the main text. shortcut rune // The key to select the list item directly, 0 if there is no shortcut. selected func() // The optional function which is called when the item is selected. reference interface{} // An optional reference object. sync.RWMutex } // NewListItem returns a new item for a list. func NewListItem(mainText string) *ListItem { return &ListItem{ mainText: []byte(mainText), } } // SetMainBytes sets the main text of the list item. func (l *ListItem) SetMainBytes(val []byte) { l.Lock() defer l.Unlock() l.mainText = val } // SetMainText sets the main text of the list item. func (l *ListItem) SetMainText(val string) { l.SetMainBytes([]byte(val)) } // GetMainBytes returns the item's main text. func (l *ListItem) GetMainBytes() []byte { l.RLock() defer l.RUnlock() return l.mainText } // GetMainText returns the item's main text. func (l *ListItem) GetMainText() string { return string(l.GetMainBytes()) } // SetSecondaryBytes sets a secondary text to be shown underneath the main text. func (l *ListItem) SetSecondaryBytes(val []byte) { l.Lock() defer l.Unlock() l.secondaryText = val } // SetSecondaryText sets a secondary text to be shown underneath the main text. func (l *ListItem) SetSecondaryText(val string) { l.SetSecondaryBytes([]byte(val)) } // GetSecondaryBytes returns the item's secondary text. func (l *ListItem) GetSecondaryBytes() []byte { l.RLock() defer l.RUnlock() return l.secondaryText } // GetSecondaryText returns the item's secondary text. func (l *ListItem) GetSecondaryText() string { return string(l.GetSecondaryBytes()) } // SetShortcut sets the key to select the ListItem directly, 0 if there is no shortcut. func (l *ListItem) SetShortcut(val rune) { l.Lock() defer l.Unlock() l.shortcut = val } // GetShortcut returns the ListItem's shortcut. func (l *ListItem) GetShortcut() rune { l.RLock() defer l.RUnlock() return l.shortcut } // SetSelectedFunc sets a function which is called when the ListItem is selected. func (l *ListItem) SetSelectedFunc(handler func()) { l.Lock() defer l.Unlock() l.selected = handler } // SetReference allows you to store a reference of any type in the item func (l *ListItem) SetReference(val interface{}) { l.Lock() defer l.Unlock() l.reference = val } // GetReference returns the item's reference object. func (l *ListItem) GetReference() interface{} { l.RLock() defer l.RUnlock() return l.reference } // List displays rows of items, each of which can be selected. type List struct { *Box *ContextMenu // The items of the list. items []*ListItem // The index of the currently selected item. currentItem int // Whether or not to show the secondary item texts. showSecondaryText bool // The item main text color. mainTextColor tcell.Color // The item secondary text color. secondaryTextColor tcell.Color // The item shortcut text color. shortcutColor tcell.Color // The text color for selected items. selectedTextColor tcell.Color // The style attributes for selected items. selectedTextAttributes tcell.AttrMask // Visibility of the scroll bar. scrollBarVisibility ScrollBarVisibility // The scroll bar color. scrollBarColor tcell.Color // The background color for selected items. selectedBackgroundColor tcell.Color // If true, the selection is only shown when the list has focus. selectedFocusOnly bool // If true, the selection must remain visible when scrolling. selectedAlwaysVisible bool // If true, the selection must remain centered when scrolling. selectedAlwaysCentered bool // If true, the entire row is highlighted when selected. highlightFullLine bool // Whether or not navigating the list will wrap around. wrapAround bool // Whether or not hovering over an item will highlight it. hover bool // The number of list items and columns by which the list is scrolled // down/to the right. itemOffset, columnOffset int // An optional function which is called when the user has navigated to a list // item. changed func(index int, item *ListItem) // An optional function which is called when a list item was selected. This // function will be called even if the list item defines its own callback. selected func(index int, item *ListItem) // An optional function which is called when the user presses the Escape key. done func() // The height of the list the last time it was drawn. height int sync.RWMutex } // NewList returns a new form. func NewList() *List { l := &List{ Box: NewBox(), showSecondaryText: true, scrollBarVisibility: ScrollBarAuto, mainTextColor: Styles.PrimaryTextColor, secondaryTextColor: Styles.TertiaryTextColor, shortcutColor: Styles.SecondaryTextColor, selectedTextColor: Styles.PrimitiveBackgroundColor, scrollBarColor: Styles.ScrollBarColor, selectedBackgroundColor: Styles.PrimaryTextColor, } l.ContextMenu = NewContextMenu(l) l.focus = l return l } // SetCurrentItem sets the currently selected item by its index, starting at 0 // for the first item. If a negative index is provided, items are referred to // from the back (-1 = last item, -2 = second-to-last item, and so on). Out of // range indices are clamped to the beginning/end. // // Calling this function triggers a "changed" event if the selection changes. func (l *List) SetCurrentItem(index int) { l.Lock() if index < 0 { index = len(l.items) + index } if index >= len(l.items) { index = len(l.items) - 1 } if index < 0 { index = 0 } previousItem := l.currentItem l.currentItem = index l.updateOffset() if index != previousItem && index < len(l.items) && l.changed != nil { item := l.items[index] l.Unlock() l.changed(index, item) } else { l.Unlock() } } // GetCurrentItem returns the currently selected list item, // Returns nil if no item is selected. func (l *List) GetCurrentItem() *ListItem { l.RLock() defer l.RUnlock() if len(l.items) == 0 || l.currentItem >= len(l.items) { return nil } return l.items[l.currentItem] } // GetCurrentItemIndex returns the index of the currently selected list item, // starting at 0 for the first item and its struct. func (l *List) GetCurrentItemIndex() int { l.RLock() defer l.RUnlock() return l.currentItem } // GetItems returns all list items. func (l *List) GetItems() []*ListItem { l.RLock() defer l.RUnlock() return l.items } // RemoveItem removes the item with the given index (starting at 0) from the // list. If a negative index is provided, items are referred to from the back // (-1 = last item, -2 = second-to-last item, and so on). Out of range indices // are clamped to the beginning/end, i.e. unless the list is empty, an item is // always removed. // // The currently selected item is shifted accordingly. If it is the one that is // removed, a "changed" event is fired. func (l *List) RemoveItem(index int) { l.Lock() if len(l.items) == 0 { l.Unlock() return } // Adjust index. if index < 0 { index = len(l.items) + index } if index >= len(l.items) { index = len(l.items) - 1 } if index < 0 { index = 0 } // Remove item. l.items = append(l.items[:index], l.items[index+1:]...) // If there is nothing left, we're done. if len(l.items) == 0 { l.Unlock() return } // Shift current item. previousItem := l.currentItem if l.currentItem >= index && l.currentItem > 0 { l.currentItem-- } // Fire "changed" event for removed items. if previousItem == index && index < len(l.items) && l.changed != nil { item := l.items[l.currentItem] l.Unlock() l.changed(l.currentItem, item) } else { l.Unlock() } } // SetOffset sets the number of list items and columns by which the list is // scrolled down/to the right. func (l *List) SetOffset(items, columns int) { l.Lock() defer l.Unlock() if items < 0 { items = 0 } if columns < 0 { columns = 0 } l.itemOffset, l.columnOffset = items, columns } // GetOffset returns the number of list items and columns by which the list is // scrolled down/to the right. func (l *List) GetOffset() (int, int) { l.Lock() defer l.Unlock() return l.itemOffset, l.columnOffset } // SetMainTextColor sets the color of the items' main text. func (l *List) SetMainTextColor(color tcell.Color) { l.Lock() defer l.Unlock() l.mainTextColor = color } // SetSecondaryTextColor sets the color of the items' secondary text. func (l *List) SetSecondaryTextColor(color tcell.Color) { l.Lock() defer l.Unlock() l.secondaryTextColor = color } // SetShortcutColor sets the color of the items' shortcut. func (l *List) SetShortcutColor(color tcell.Color) { l.Lock() defer l.Unlock() l.shortcutColor = color } // SetSelectedTextColor sets the text color of selected items. func (l *List) SetSelectedTextColor(color tcell.Color) { l.Lock() defer l.Unlock() l.selectedTextColor = color } // SetSelectedTextAttributes sets the style attributes of selected items. func (l *List) SetSelectedTextAttributes(attr tcell.AttrMask) { l.Lock() defer l.Unlock() l.selectedTextAttributes = attr } // SetSelectedBackgroundColor sets the background color of selected items. func (l *List) SetSelectedBackgroundColor(color tcell.Color) { l.Lock() defer l.Unlock() l.selectedBackgroundColor = color } // SetSelectedFocusOnly sets a flag which determines when the currently selected // list item is highlighted. If set to true, selected items are only highlighted // when the list has focus. If set to false, they are always highlighted. func (l *List) SetSelectedFocusOnly(focusOnly bool) { l.Lock() defer l.Unlock() l.selectedFocusOnly = focusOnly } // SetSelectedAlwaysVisible sets a flag which determines whether the currently // selected list item must remain visible when scrolling. func (l *List) SetSelectedAlwaysVisible(alwaysVisible bool) { l.Lock() defer l.Unlock() l.selectedAlwaysVisible = alwaysVisible } // SetSelectedAlwaysCentered sets a flag which determines whether the currently // selected list item must remain centered when scrolling. func (l *List) SetSelectedAlwaysCentered(alwaysCentered bool) { l.Lock() defer l.Unlock() l.selectedAlwaysCentered = alwaysCentered } // SetHighlightFullLine sets a flag which determines whether the colored // background of selected items spans the entire width of the view. If set to // true, the highlight spans the entire view. If set to false, only the text of // the selected item from beginning to end is highlighted. func (l *List) SetHighlightFullLine(highlight bool) { l.Lock() defer l.Unlock() l.highlightFullLine = highlight } // ShowSecondaryText determines whether or not to show secondary item texts. func (l *List) ShowSecondaryText(show bool) { l.Lock() defer l.Unlock() l.showSecondaryText = show return } // SetScrollBarVisibility specifies the display of the scroll bar. func (l *List) SetScrollBarVisibility(visibility ScrollBarVisibility) { l.Lock() defer l.Unlock() l.scrollBarVisibility = visibility } // SetScrollBarColor sets the color of the scroll bar. func (l *List) SetScrollBarColor(color tcell.Color) { l.Lock() defer l.Unlock() l.scrollBarColor = color } // SetHover sets the flag that determines whether hovering over an item will // highlight it (without triggering callbacks set with SetSelectedFunc). func (l *List) SetHover(hover bool) { l.Lock() defer l.Unlock() l.hover = hover } // SetWrapAround sets the flag that determines whether navigating the list will // wrap around. That is, navigating downwards on the last item will move the // selection to the first item (similarly in the other direction). If set to // false, the selection won't change when navigating downwards on the last item // or navigating upwards on the first item. func (l *List) SetWrapAround(wrapAround bool) { l.Lock() defer l.Unlock() l.wrapAround = wrapAround } // SetChangedFunc sets the function which is called when the user navigates to // a list item. The function receives the item's index in the list of items // (starting with 0) and the list item. // // This function is also called when the first item is added or when // SetCurrentItem() is called. func (l *List) SetChangedFunc(handler func(index int, item *ListItem)) { l.Lock() defer l.Unlock() l.changed = handler } // SetSelectedFunc sets the function which is called when the user selects a // list item by pressing Enter on the current selection. The function receives // the item's index in the list of items (starting with 0) and its struct. func (l *List) SetSelectedFunc(handler func(int, *ListItem)) { l.Lock() defer l.Unlock() l.selected = handler } // SetDoneFunc sets a function which is called when the user presses the Escape // key. func (l *List) SetDoneFunc(handler func()) { l.Lock() defer l.Unlock() l.done = handler } // AddItem calls InsertItem() with an index of -1. func (l *List) AddItem(item *ListItem) { l.InsertItem(-1, item) } // InsertItem adds a new item to the list at the specified index. An index of 0 // will insert the item at the beginning, an index of 1 before the second item, // and so on. An index of GetItemCount() or higher will insert the item at the // end of the list. Negative indices are also allowed: An index of -1 will // insert the item at the end of the list, an index of -2 before the last item, // and so on. An index of -GetItemCount()-1 or lower will insert the item at the // beginning. // // An item has a main text which will be highlighted when selected. It also has // a secondary text which is shown underneath the main text (if it is set to // visible) but which may remain empty. // // The shortcut is a key binding. If the specified rune is entered, the item // is selected immediately. Set to 0 for no binding. // // The "selected" callback will be invoked when the user selects the item. You // may provide nil if no such callback is needed or if all events are handled // through the selected callback set with SetSelectedFunc(). // // The currently selected item will shift its position accordingly. If the list // was previously empty, a "changed" event is fired because the new item becomes // selected. func (l *List) InsertItem(index int, item *ListItem) { l.Lock() // Shift index to range. if index < 0 { index = len(l.items) + index + 1 } if index < 0 { index = 0 } else if index > len(l.items) { index = len(l.items) } // Shift current item. if l.currentItem < len(l.items) && l.currentItem >= index { l.currentItem++ } // Insert item (make space for the new item, then shift and insert). l.items = append(l.items, nil) if index < len(l.items)-1 { // -1 because l.items has already grown by one item. copy(l.items[index+1:], l.items[index:]) } l.items[index] = item // Fire a "change" event for the first item in the list. if len(l.items) == 1 && l.changed != nil { item := l.items[0] l.Unlock() l.changed(0, item) } else { l.Unlock() } } // GetItem returns the ListItem at the given index. // Returns nil when index is out of bounds. func (l *List) GetItem(index int) *ListItem { if index > len(l.items)-1 { return nil } return l.items[index] } // GetItemCount returns the number of items in the list. func (l *List) GetItemCount() int { l.RLock() defer l.RUnlock() return len(l.items) } // GetItemText returns an item's texts (main and secondary). Panics if the index // is out of range. func (l *List) GetItemText(index int) (main, secondary string) { l.RLock() defer l.RUnlock() return string(l.items[index].mainText), string(l.items[index].secondaryText) } // SetItemText sets an item's main and secondary text. Panics if the index is // out of range. func (l *List) SetItemText(index int, main, secondary string) { l.Lock() defer l.Unlock() item := l.items[index] item.mainText = []byte(main) item.secondaryText = []byte(secondary) } // SetItemEnabled sets whether an item is selectable. Panics if the index is // out of range. func (l *List) SetItemEnabled(index int, enabled bool) { l.Lock() defer l.Unlock() item := l.items[index] item.disabled = !enabled } // FindItems searches the main and secondary texts for the given strings and // returns a list of item indices in which those strings are found. One of the // two search strings may be empty, it will then be ignored. Indices are always // returned in ascending order. // // If mustContainBoth is set to true, mainSearch must be contained in the main // text AND secondarySearch must be contained in the secondary text. If it is // false, only one of the two search strings must be contained. // // Set ignoreCase to true for case-insensitive search. func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) { l.RLock() defer l.RUnlock() if mainSearch == "" && secondarySearch == "" { return } if ignoreCase { mainSearch = strings.ToLower(mainSearch) secondarySearch = strings.ToLower(secondarySearch) } mainSearchBytes := []byte(mainSearch) secondarySearchBytes := []byte(secondarySearch) for index, item := range l.items { mainText := item.mainText secondaryText := item.secondaryText if ignoreCase { mainText = bytes.ToLower(mainText) secondaryText = bytes.ToLower(secondaryText) } // strings.Contains() always returns true for a "" search. mainContained := bytes.Contains(mainText, mainSearchBytes) secondaryContained := bytes.Contains(secondaryText, secondarySearchBytes) if mustContainBoth && mainContained && secondaryContained || !mustContainBoth && (len(mainText) > 0 && mainContained || len(secondaryText) > 0 && secondaryContained) { indices = append(indices, index) } } return } // Clear removes all items from the list. func (l *List) Clear() { l.Lock() defer l.Unlock() l.items = nil l.currentItem = 0 l.itemOffset = 0 l.columnOffset = 0 } // Focus is called by the application when the primitive receives focus. func (l *List) Focus(delegate func(p Primitive)) { l.Box.Focus(delegate) if l.ContextMenu.open { delegate(l.ContextMenu.list) } } // HasFocus returns whether or not this primitive has focus. func (l *List) HasFocus() bool { if l.ContextMenu.open { return l.ContextMenu.list.HasFocus() } l.RLock() defer l.RUnlock() return l.hasFocus } // Transform modifies the current selection. func (l *List) Transform(tr Transformation) { l.Lock() previousItem := l.currentItem l.transform(tr) if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil { item := l.items[l.currentItem] l.Unlock() l.changed(l.currentItem, item) } else { l.Unlock() } } func (l *List) transform(tr Transformation) { var decreasing bool pageItems := l.height if l.showSecondaryText { pageItems /= 2 } if pageItems < 1 { pageItems = 1 } switch tr { case TransformFirstItem: l.currentItem = 0 l.itemOffset = 0 decreasing = true case TransformLastItem: l.currentItem = len(l.items) - 1 case TransformPreviousItem: l.currentItem-- decreasing = true case TransformNextItem: l.currentItem++ case TransformPreviousPage: l.currentItem -= pageItems decreasing = true case TransformNextPage: l.currentItem += pageItems l.itemOffset += pageItems } for i := 0; i < len(l.items); i++ { if l.currentItem < 0 { if l.wrapAround { l.currentItem = len(l.items) - 1 } else { l.currentItem = 0 l.itemOffset = 0 } } else if l.currentItem >= len(l.items) { if l.wrapAround { l.currentItem = 0 l.itemOffset = 0 } else { l.currentItem = len(l.items) - 1 } } item := l.items[l.currentItem] if !item.disabled && (item.shortcut > 0 || len(item.mainText) > 0 || len(item.secondaryText) > 0) { break } if decreasing { l.currentItem-- } else { l.currentItem++ } } l.updateOffset() } func (l *List) updateOffset() { _, _, _, l.height = l.GetInnerRect() h := l.height if l.selectedAlwaysCentered { h /= 2 } if l.currentItem < l.itemOffset { l.itemOffset = l.currentItem } else if l.showSecondaryText { if 2*(l.currentItem-l.itemOffset) >= h-1 { l.itemOffset = (2*l.currentItem + 3 - h) / 2 } } else { if l.currentItem-l.itemOffset >= h { l.itemOffset = l.currentItem + 1 - h } } if l.showSecondaryText { if l.itemOffset > len(l.items)-(l.height/2) { l.itemOffset = len(l.items) - l.height/2 } } else { if l.itemOffset > len(l.items)-l.height { l.itemOffset = len(l.items) - l.height } } if l.itemOffset < 0 { l.itemOffset = 0 } // Maximum width of item text maxWidth := 0 for _, option := range l.items { strWidth := TaggedTextWidth(option.mainText) secondaryWidth := TaggedTextWidth(option.secondaryText) if secondaryWidth > strWidth { strWidth = secondaryWidth } if option.shortcut != 0 { strWidth += 4 } if strWidth > maxWidth { maxWidth = strWidth } } // Additional width for scroll bar addWidth := 0 if l.scrollBarVisibility == ScrollBarAlways || (l.scrollBarVisibility == ScrollBarAuto && ((!l.showSecondaryText && len(l.items) > l.innerHeight) || (l.showSecondaryText && len(l.items) > l.innerHeight/2))) { addWidth = 1 } if l.columnOffset > (maxWidth-l.innerWidth)+addWidth { l.columnOffset = (maxWidth - l.innerWidth) + addWidth } if l.columnOffset < 0 { l.columnOffset = 0 } } // Draw draws this primitive onto the screen. func (l *List) Draw(screen tcell.Screen) { if !l.GetVisible() { return } l.Box.Draw(screen) hasFocus := l.GetFocusable().HasFocus() l.Lock() defer l.Unlock() // Determine the dimensions. x, y, width, height := l.GetInnerRect() bottomLimit := y + height l.height = height screenWidth, _ := screen.Size() scrollBarHeight := height scrollBarX := x + (width - 1) + l.paddingLeft + l.paddingRight if scrollBarX > screenWidth-1 { scrollBarX = screenWidth - 1 } // Halve scroll bar height when drawing two lines per list item. if l.showSecondaryText { scrollBarHeight /= 2 } // Do we show any shortcuts? var showShortcuts bool for _, item := range l.items { if item.shortcut != 0 { showShortcuts = true x += 4 width -= 4 break } } // Adjust offset to keep the current selection in view. if l.selectedAlwaysVisible || l.selectedAlwaysCentered { l.updateOffset() } scrollBarCursor := int(float64(len(l.items)) * (float64(l.itemOffset) / float64(len(l.items)-height))) // Draw the list items. for index, item := range l.items { if index < l.itemOffset { continue } if y >= bottomLimit { break } mainText := item.mainText secondaryText := item.secondaryText if l.columnOffset > 0 { if l.columnOffset < len(mainText) { mainText = mainText[l.columnOffset:] } else { mainText = nil } if l.columnOffset < len(secondaryText) { secondaryText = secondaryText[l.columnOffset:] } else { secondaryText = nil } } if len(item.mainText) == 0 && len(item.secondaryText) == 0 && item.shortcut == 0 { // Divider Print(screen, bytes.Repeat([]byte(string(tcell.RuneHLine)), width+l.paddingLeft+l.paddingRight), x-l.paddingLeft, y, width+l.paddingLeft+l.paddingRight, AlignLeft, l.mainTextColor) RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor) y++ continue } else if item.disabled { // Shortcuts. if showShortcuts && item.shortcut != 0 { Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, tcell.ColorDarkSlateGray.TrueColor()) } // Main text. Print(screen, mainText, x, y, width, AlignLeft, tcell.ColorGray.TrueColor()) RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor) y++ continue } // Shortcuts. if showShortcuts && item.shortcut != 0 { Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, l.shortcutColor) } // Main text. Print(screen, mainText, x, y, width, AlignLeft, l.mainTextColor) // Background color of selected text. if index == l.currentItem && (!l.selectedFocusOnly || hasFocus) { textWidth := width if !l.highlightFullLine { if w := TaggedTextWidth(mainText); w < textWidth { textWidth = w } } for bx := 0; bx < textWidth; bx++ { m, c, style, _ := screen.GetContent(x+bx, y) fg, _, _ := style.Decompose() if fg == l.mainTextColor { fg = l.selectedTextColor } style = SetAttributes(style.Background(l.selectedBackgroundColor).Foreground(fg), l.selectedTextAttributes) screen.SetContent(x+bx, y, m, c, style) } } RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor) y++ if y >= bottomLimit { break } // Secondary text. if l.showSecondaryText { Print(screen, secondaryText, x, y, width, AlignLeft, l.secondaryTextColor) RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor) y++ } } // Overdraw scroll bar when necessary. for y < bottomLimit { RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, bottomLimit-y, l.hasFocus, l.scrollBarColor) y++ } // Draw context menu. if hasFocus && l.ContextMenu.open { ctx := l.ContextMenuList() x, y, width, height = l.GetInnerRect() // What's the longest option text? maxWidth := 0 for _, option := range ctx.items { strWidth := TaggedTextWidth(option.mainText) if option.shortcut != 0 { strWidth += 4 } if strWidth > maxWidth { maxWidth = strWidth } } lheight := len(ctx.items) lwidth := maxWidth // Add space for borders lwidth += 2 lheight += 2 lwidth += ctx.paddingLeft + ctx.paddingRight lheight += ctx.paddingTop + ctx.paddingBottom cx, cy := l.ContextMenu.x, l.ContextMenu.y if cx < 0 || cy < 0 { offsetX := 7 if showShortcuts { offsetX += 4 } offsetY := l.currentItem if l.showSecondaryText { offsetY *= 2 } x, y, _, _ := l.GetInnerRect() cx, cy = x+offsetX, y+offsetY } _, sheight := screen.Size() if cy+lheight >= sheight && cy-2 > lheight-cy { for i := (cy + lheight) - sheight; i > 0; i-- { cy-- if cy+lheight < sheight { break } } if cy < 0 { cy = 0 } } if cy+lheight >= sheight { lheight = sheight - cy } if ctx.scrollBarVisibility == ScrollBarAlways || (ctx.scrollBarVisibility == ScrollBarAuto && len(ctx.items) > lheight) { lwidth++ // Add space for scroll bar } ctx.SetRect(cx, cy, lwidth, lheight) ctx.Draw(screen) } } // InputHandler returns the handler for this primitive. func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { l.Lock() if HitShortcut(event, Keys.Cancel) { if l.ContextMenu.open { l.Unlock() l.ContextMenu.hide(setFocus) return } if l.done != nil { l.Unlock() l.done() } else { l.Unlock() } return } else if HitShortcut(event, Keys.Select, Keys.Select2) { if l.currentItem >= 0 && l.currentItem < len(l.items) { item := l.items[l.currentItem] if !item.disabled { if item.selected != nil { l.Unlock() item.selected() l.Lock() } if l.selected != nil { l.Unlock() l.selected(l.currentItem, item) l.Lock() } } } } else if HitShortcut(event, Keys.ShowContextMenu) { defer l.ContextMenu.show(l.currentItem, -1, -1, setFocus) } else if len(l.items) == 0 { l.Unlock() return } if event.Key() == tcell.KeyRune { ch := event.Rune() if ch != ' ' { // It's not a space bar. Is it a shortcut? for index, item := range l.items { if !item.disabled && item.shortcut == ch { // We have a shortcut. l.currentItem = index item := l.items[l.currentItem] if item.selected != nil { l.Unlock() item.selected() l.Lock() } if l.selected != nil { l.Unlock() l.selected(l.currentItem, item) l.Lock() } l.Unlock() return } } } } previousItem := l.currentItem if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) { l.transform(TransformFirstItem) } else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) { l.transform(TransformLastItem) } else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MovePreviousField) { l.transform(TransformPreviousItem) } else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) { l.transform(TransformNextItem) } else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) { l.columnOffset-- l.updateOffset() } else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) { l.columnOffset++ l.updateOffset() } else if HitShortcut(event, Keys.MovePreviousPage) { l.transform(TransformPreviousPage) } else if HitShortcut(event, Keys.MoveNextPage) { l.transform(TransformNextPage) } if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil { item := l.items[l.currentItem] l.Unlock() l.changed(l.currentItem, item) } else { l.Unlock() } }) } // indexAtY returns the index of the list item found at the given Y position // or a negative value if there is no such list item. func (l *List) indexAtY(y int) int { _, rectY, _, height := l.GetInnerRect() if y < rectY || y >= rectY+height { return -1 } index := y - rectY if l.showSecondaryText { index /= 2 } index += l.itemOffset if index >= len(l.items) { return -1 } return index } // indexAtPoint returns the index of the list item found at the given position // or a negative value if there is no such list item. func (l *List) indexAtPoint(x, y int) int { rectX, rectY, width, height := l.GetInnerRect() if x < rectX || x >= rectX+width || y < rectY || y >= rectY+height { return -1 } index := y - rectY if l.showSecondaryText { index /= 2 } index += l.itemOffset if index >= len(l.items) { return -1 } return index } // MouseHandler returns the mouse handler for this primitive. func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { l.Lock() // Pass events to context menu. if l.ContextMenuVisible() && l.ContextMenuList().InRect(event.Position()) { defer l.ContextMenuList().MouseHandler()(action, event, setFocus) consumed = true l.Unlock() return } if !l.InRect(event.Position()) { l.Unlock() return false, nil } // Process mouse event. switch action { case MouseLeftClick: if l.ContextMenuVisible() { defer l.ContextMenu.hide(setFocus) consumed = true l.Unlock() return } l.Unlock() setFocus(l) l.Lock() index := l.indexAtPoint(event.Position()) if index != -1 { item := l.items[index] if !item.disabled { l.currentItem = index if item.selected != nil { l.Unlock() item.selected() l.Lock() } if l.selected != nil { l.Unlock() l.selected(index, item) l.Lock() } if index != l.currentItem && l.changed != nil { l.Unlock() l.changed(index, item) l.Lock() } } } consumed = true case MouseMiddleClick: if l.ContextMenu.open { defer l.ContextMenu.hide(setFocus) consumed = true l.Unlock() return } case MouseRightDown: if len(l.ContextMenuList().items) == 0 { l.Unlock() return } x, y := event.Position() index := l.indexAtPoint(event.Position()) if index != -1 { item := l.items[index] if !item.disabled { l.currentItem = index if index != l.currentItem && l.changed != nil { l.Unlock() l.changed(index, item) l.Lock() } } } defer l.ContextMenu.show(l.currentItem, x, y, setFocus) l.ContextMenu.drag = true consumed = true case MouseMove: if l.hover { _, y := event.Position() index := l.indexAtY(y) if index >= 0 { item := l.items[index] if !item.disabled { l.currentItem = index } } consumed = true } case MouseScrollUp: if l.itemOffset > 0 { l.itemOffset-- } consumed = true case MouseScrollDown: lines := len(l.items) - l.itemOffset if l.showSecondaryText { lines *= 2 } if _, _, _, height := l.GetInnerRect(); lines > height { l.itemOffset++ } consumed = true } l.Unlock() return }) }
package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // Modal is a centered message window used to inform the user or prompt them // for an immediate decision. It needs to have at least one button (added via // AddButtons) or it will never disappear. You may change the title and // appearance of the window by modifying the Frame returned by GetFrame. You // may include additional elements within the window by modifying the Form // returned by GetForm. type Modal struct { *Box // The Frame embedded in the Modal. frame *Frame // The Form embedded in the Modal's Frame. form *Form // The message text (original, not word-wrapped). text string // The text color. textColor tcell.Color // The optional callback for when the user clicked one of the buttons. It // receives the index of the clicked button and the button's label. done func(buttonIndex int, buttonLabel string) sync.RWMutex } // NewModal returns a new centered message window. func NewModal() *Modal { m := &Modal{ Box: NewBox(), textColor: Styles.PrimaryTextColor, } m.form = NewForm() m.form.SetButtonsAlign(AlignCenter) m.form.SetButtonBackgroundColor(Styles.PrimitiveBackgroundColor) m.form.SetButtonTextColor(Styles.PrimaryTextColor) m.form.SetBackgroundColor(Styles.ContrastBackgroundColor) m.form.SetPadding(0, 0, 0, 0) m.form.SetCancelFunc(func() { if m.done != nil { m.done(-1, "") } }) m.frame = NewFrame(m.form) m.frame.SetBorder(true) m.frame.SetBorders(0, 0, 1, 0, 0, 0) m.frame.SetBackgroundColor(Styles.ContrastBackgroundColor) m.frame.SetPadding(1, 1, 1, 1) m.focus = m return m } // SetBackgroundColor sets the color of the Modal Frame background. func (m *Modal) SetBackgroundColor(color tcell.Color) { m.Lock() defer m.Unlock() m.form.SetBackgroundColor(color) m.frame.SetBackgroundColor(color) } // SetTextColor sets the color of the message text. func (m *Modal) SetTextColor(color tcell.Color) { m.Lock() defer m.Unlock() m.textColor = color } // SetButtonBackgroundColor sets the background color of the buttons. func (m *Modal) SetButtonBackgroundColor(color tcell.Color) { m.Lock() defer m.Unlock() m.form.SetButtonBackgroundColor(color) } // SetButtonTextColor sets the color of the button texts. func (m *Modal) SetButtonTextColor(color tcell.Color) { m.Lock() defer m.Unlock() m.form.SetButtonTextColor(color) } // SetDoneFunc sets a handler which is called when one of the buttons was // pressed. It receives the index of the button as well as its label text. The // handler is also called when the user presses the Escape key. The index will // then be negative and the label text an empty string. func (m *Modal) SetDoneFunc(handler func(buttonIndex int, buttonLabel string)) { m.Lock() defer m.Unlock() m.done = handler } // SetText sets the message text of the window. The text may contain line // breaks. Note that words are wrapped, too, based on the final size of the // window. func (m *Modal) SetText(text string) { m.Lock() defer m.Unlock() m.text = text } // GetForm returns the Form embedded in the window. The returned Form may be // modified to include additional elements (e.g. AddInputField, AddFormItem). func (m *Modal) GetForm() *Form { m.RLock() defer m.RUnlock() return m.form } // GetFrame returns the Frame embedded in the window. func (m *Modal) GetFrame() *Frame { m.RLock() defer m.RUnlock() return m.frame } // AddButtons adds buttons to the window. There must be at least one button and // a "done" handler so the window can be closed again. func (m *Modal) AddButtons(labels []string) { m.Lock() defer m.Unlock() for index, label := range labels { func(i int, l string) { m.form.AddButton(label, func() { if m.done != nil { m.done(i, l) } }) button := m.form.GetButton(m.form.GetButtonCount() - 1) button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyDown, tcell.KeyRight: return tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone) case tcell.KeyUp, tcell.KeyLeft: return tcell.NewEventKey(tcell.KeyBacktab, 0, tcell.ModNone) } return event }) }(index, label) } } // ClearButtons removes all buttons from the window. func (m *Modal) ClearButtons() { m.Lock() defer m.Unlock() m.form.ClearButtons() } // SetFocus shifts the focus to the button with the given index. func (m *Modal) SetFocus(index int) { m.Lock() defer m.Unlock() m.form.SetFocus(index) } // Focus is called when this primitive receives focus. func (m *Modal) Focus(delegate func(p Primitive)) { delegate(m.form) } // HasFocus returns whether or not this primitive has focus. func (m *Modal) HasFocus() bool { return m.GetForm().HasFocus() } // Draw draws this primitive onto the screen. func (m *Modal) Draw(screen tcell.Screen) { if !m.GetVisible() { return } formItemCount := m.form.GetFormItemCount() m.Lock() defer m.Unlock() // Calculate the width of this Modal. buttonsWidth := 0 for _, button := range m.form.buttons { buttonsWidth += TaggedTextWidth(button.label) + 4 + 2 } buttonsWidth -= 2 screenWidth, screenHeight := screen.Size() width := screenWidth / 3 if width < buttonsWidth { width = buttonsWidth } // width is now without the box border. // Reset the text and find out how wide it is. m.frame.Clear() lines := WordWrap(m.text, width) for _, line := range lines { m.frame.AddText(line, true, AlignCenter, m.textColor) } // Set the Modal's position and size. height := len(lines) + (formItemCount * 2) + 6 width += 4 x := (screenWidth - width) / 2 y := (screenHeight - height) / 2 m.SetRect(x, y, width, height) // Draw the frame. m.frame.SetRect(x, y, width, height) m.frame.Draw(screen) } // MouseHandler returns the mouse handler for this primitive. func (m *Modal) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return m.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { // Pass mouse events on to the form. consumed, capture = m.form.MouseHandler()(action, event, setFocus) if !consumed && action == MouseLeftClick && m.InRect(event.Position()) { setFocus(m) consumed = true } return }) }
package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // panel represents a single panel of a Panels object. type panel struct { Name string // The panel's name. Item Primitive // The panel's primitive. Resize bool // Whether or not to resize the panel when it is drawn. Visible bool // Whether or not this panel is visible. } // Panels is a container for other primitives often used as the application's // root primitive. It allows to easily switch the visibility of the contained // primitives. type Panels struct { *Box // The contained panels. (Visible) panels are drawn from back to front. panels []*panel // We keep a reference to the function which allows us to set the focus to // a newly visible panel. setFocus func(p Primitive) // An optional handler which is called whenever the visibility or the order of // panels changes. changed func() sync.RWMutex } // NewPanels returns a new Panels object. func NewPanels() *Panels { p := &Panels{ Box: NewBox(), } p.focus = p return p } // SetChangedFunc sets a handler which is called whenever the visibility or the // order of any visible panels changes. This can be used to redraw the panels. func (p *Panels) SetChangedFunc(handler func()) { p.Lock() defer p.Unlock() p.changed = handler } // GetPanelCount returns the number of panels currently stored in this object. func (p *Panels) GetPanelCount() int { p.RLock() defer p.RUnlock() return len(p.panels) } // AddPanel adds a new panel with the given name and primitive. If there was // previously a panel with the same name, it is overwritten. Leaving the name // empty may cause conflicts in other functions so always specify a non-empty // name. // // Visible panels will be drawn in the order they were added (unless that order // was changed in one of the other functions). If "resize" is set to true, the // primitive will be set to the size available to the Panels primitive whenever // the panels are drawn. func (p *Panels) AddPanel(name string, item Primitive, resize, visible bool) { hasFocus := p.HasFocus() p.Lock() defer p.Unlock() var added bool for i, pg := range p.panels { if pg.Name == name { p.panels[i] = &panel{Item: item, Name: name, Resize: resize, Visible: visible} added = true break } } if !added { p.panels = append(p.panels, &panel{Item: item, Name: name, Resize: resize, Visible: visible}) } if p.changed != nil { p.Unlock() p.changed() p.Lock() } if hasFocus { p.Unlock() p.Focus(p.setFocus) p.Lock() } } // RemovePanel removes the panel with the given name. If that panel was the only // visible panel, visibility is assigned to the last panel. func (p *Panels) RemovePanel(name string) { hasFocus := p.HasFocus() p.Lock() defer p.Unlock() var isVisible bool for index, panel := range p.panels { if panel.Name == name { isVisible = panel.Visible p.panels = append(p.panels[:index], p.panels[index+1:]...) if panel.Visible && p.changed != nil { p.Unlock() p.changed() p.Lock() } break } } if isVisible { for index, panel := range p.panels { if index < len(p.panels)-1 { if panel.Visible { break // There is a remaining visible panel. } } else { panel.Visible = true // We need at least one visible panel. } } } if hasFocus { p.Unlock() p.Focus(p.setFocus) p.Lock() } } // HasPanel returns true if a panel with the given name exists in this object. func (p *Panels) HasPanel(name string) bool { p.RLock() defer p.RUnlock() for _, panel := range p.panels { if panel.Name == name { return true } } return false } // ShowPanel sets a panel's visibility to "true" (in addition to any other panels // which are already visible). func (p *Panels) ShowPanel(name string) { hasFocus := p.HasFocus() p.Lock() defer p.Unlock() for _, panel := range p.panels { if panel.Name == name { panel.Visible = true if p.changed != nil { p.Unlock() p.changed() p.Lock() } break } } if hasFocus { p.Unlock() p.Focus(p.setFocus) p.Lock() } } // HidePanel sets a panel's visibility to "false". func (p *Panels) HidePanel(name string) { hasFocus := p.HasFocus() p.Lock() defer p.Unlock() for _, panel := range p.panels { if panel.Name == name { panel.Visible = false if p.changed != nil { p.Unlock() p.changed() p.Lock() } break } } if hasFocus { p.Unlock() p.Focus(p.setFocus) p.Lock() } } // SetCurrentPanel sets a panel's visibility to "true" and all other panels' // visibility to "false". func (p *Panels) SetCurrentPanel(name string) { hasFocus := p.HasFocus() p.Lock() defer p.Unlock() for _, panel := range p.panels { if panel.Name == name { panel.Visible = true } else { panel.Visible = false } } if p.changed != nil { p.Unlock() p.changed() p.Lock() } if hasFocus { p.Unlock() p.Focus(p.setFocus) p.Lock() } } // SendToFront changes the order of the panels such that the panel with the given // name comes last, causing it to be drawn last with the next update (if // visible). func (p *Panels) SendToFront(name string) { hasFocus := p.HasFocus() p.Lock() defer p.Unlock() for index, panel := range p.panels { if panel.Name == name { if index < len(p.panels)-1 { p.panels = append(append(p.panels[:index], p.panels[index+1:]...), panel) } if panel.Visible && p.changed != nil { p.Unlock() p.changed() p.Lock() } break } } if hasFocus { p.Unlock() p.Focus(p.setFocus) p.Lock() } } // SendToBack changes the order of the panels such that the panel with the given // name comes first, causing it to be drawn first with the next update (if // visible). func (p *Panels) SendToBack(name string) { hasFocus := p.HasFocus() p.Lock() defer p.Unlock() for index, pg := range p.panels { if pg.Name == name { if index > 0 { p.panels = append(append([]*panel{pg}, p.panels[:index]...), p.panels[index+1:]...) } if pg.Visible && p.changed != nil { p.Unlock() p.changed() p.Lock() } break } } if hasFocus { p.Unlock() p.Focus(p.setFocus) p.Lock() } } // GetFrontPanel returns the front-most visible panel. If there are no visible // panels, ("", nil) is returned. func (p *Panels) GetFrontPanel() (name string, item Primitive) { p.RLock() defer p.RUnlock() for index := len(p.panels) - 1; index >= 0; index-- { if p.panels[index].Visible { return p.panels[index].Name, p.panels[index].Item } } return } // HasFocus returns whether or not this primitive has focus. func (p *Panels) HasFocus() bool { p.RLock() defer p.RUnlock() for _, panel := range p.panels { if panel.Item.GetFocusable().HasFocus() { return true } } return false } // Focus is called by the application when the primitive receives focus. func (p *Panels) Focus(delegate func(p Primitive)) { p.Lock() defer p.Unlock() if delegate == nil { return // We cannot delegate so we cannot focus. } p.setFocus = delegate var topItem Primitive for _, panel := range p.panels { if panel.Visible { topItem = panel.Item } } if topItem != nil { p.Unlock() delegate(topItem) p.Lock() } } // Draw draws this primitive onto the screen. func (p *Panels) Draw(screen tcell.Screen) { if !p.GetVisible() { return } p.Box.Draw(screen) p.Lock() defer p.Unlock() x, y, width, height := p.GetInnerRect() for _, panel := range p.panels { if !panel.Visible { continue } if panel.Resize { panel.Item.SetRect(x, y, width, height) } panel.Item.Draw(screen) } } // MouseHandler returns the mouse handler for this primitive. func (p *Panels) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return p.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !p.InRect(event.Position()) { return false, nil } // Pass mouse events along to the last visible panel item that takes it. for index := len(p.panels) - 1; index >= 0; index-- { panel := p.panels[index] if panel.Visible { consumed, capture = panel.Item.MouseHandler()(action, event, setFocus) if consumed { return } } } return }) } // Support backwards compatibility with Pages. type page = panel // Pages is a wrapper around Panels. // // Deprecated: This type is provided for backwards compatibility. // Developers should use Panels instead. type Pages struct { *Panels } // NewPages returns a new Panels object. // // Deprecated: This function is provided for backwards compatibility. // Developers should use NewPanels instead. func NewPages() *Pages { return &Pages{NewPanels()} } // GetPageCount returns the number of panels currently stored in this object. func (p *Pages) GetPageCount() int { return p.GetPanelCount() } // AddPage adds a new panel with the given name and primitive. func (p *Pages) AddPage(name string, item Primitive, resize, visible bool) { p.AddPanel(name, item, resize, visible) } // AddAndSwitchToPage calls Add(), then SwitchTo() on that newly added panel. func (p *Pages) AddAndSwitchToPage(name string, item Primitive, resize bool) { p.AddPanel(name, item, resize, true) p.SetCurrentPanel(name) } // RemovePage removes the panel with the given name. func (p *Pages) RemovePage(name string) { p.RemovePanel(name) } // HasPage returns true if a panel with the given name exists in this object. func (p *Pages) HasPage(name string) bool { return p.HasPanel(name) } // ShowPage sets a panel's visibility to "true". func (p *Pages) ShowPage(name string) { p.ShowPanel(name) } // HidePage sets a panel's visibility to "false". func (p *Pages) HidePage(name string) { p.HidePanel(name) } // SwitchToPage sets a panel's visibility to "true" and all other panels' // visibility to "false". func (p *Pages) SwitchToPage(name string) { p.SetCurrentPanel(name) } // GetFrontPage returns the front-most visible panel. func (p *Pages) GetFrontPage() (name string, item Primitive) { return p.GetFrontPanel() }
package cview import ( "math" "sync" "github.com/gdamore/tcell/v2" ) // ProgressBar indicates the progress of an operation. type ProgressBar struct { *Box // Rune to use when rendering the empty area of the progress bar. emptyRune rune // Color of the empty area of the progress bar. emptyColor tcell.Color // Rune to use when rendering the filled area of the progress bar. filledRune rune // Color of the filled area of the progress bar. filledColor tcell.Color // If set to true, instead of filling from left to right, the bar is filled // from bottom to top. vertical bool // Current progress. progress int // Progress required to fill the bar. max int sync.RWMutex } // NewProgressBar returns a new progress bar. func NewProgressBar() *ProgressBar { p := &ProgressBar{ Box: NewBox(), emptyRune: tcell.RuneBlock, emptyColor: Styles.PrimitiveBackgroundColor, filledRune: tcell.RuneBlock, filledColor: Styles.PrimaryTextColor, max: 100, } p.SetBackgroundColor(Styles.PrimitiveBackgroundColor) return p } // SetEmptyRune sets the rune used for the empty area of the progress bar. func (p *ProgressBar) SetEmptyRune(empty rune) { p.Lock() defer p.Unlock() p.emptyRune = empty } // SetEmptyColor sets the color of the empty area of the progress bar. func (p *ProgressBar) SetEmptyColor(empty tcell.Color) { p.Lock() defer p.Unlock() p.emptyColor = empty } // SetFilledRune sets the rune used for the filled area of the progress bar. func (p *ProgressBar) SetFilledRune(filled rune) { p.Lock() defer p.Unlock() p.filledRune = filled } // SetFilledColor sets the color of the filled area of the progress bar. func (p *ProgressBar) SetFilledColor(filled tcell.Color) { p.Lock() defer p.Unlock() p.filledColor = filled } // SetVertical sets the direction of the progress bar. func (p *ProgressBar) SetVertical(vertical bool) { p.Lock() defer p.Unlock() p.vertical = vertical } // SetMax sets the progress required to fill the bar. func (p *ProgressBar) SetMax(max int) { p.Lock() defer p.Unlock() p.max = max } // GetMax returns the progress required to fill the bar. func (p *ProgressBar) GetMax() int { p.RLock() defer p.RUnlock() return p.max } // AddProgress adds to the current progress. func (p *ProgressBar) AddProgress(progress int) { p.Lock() defer p.Unlock() p.progress += progress if p.progress < 0 { p.progress = 0 } else if p.progress > p.max { p.progress = p.max } } // SetProgress sets the current progress. func (p *ProgressBar) SetProgress(progress int) { p.Lock() defer p.Unlock() p.progress = progress if p.progress < 0 { p.progress = 0 } else if p.progress > p.max { p.progress = p.max } } // GetProgress gets the current progress. func (p *ProgressBar) GetProgress() int { p.RLock() defer p.RUnlock() return p.progress } // Complete returns whether the progress bar has been filled. func (p *ProgressBar) Complete() bool { p.RLock() defer p.RUnlock() return p.progress >= p.max } // Draw draws this primitive onto the screen. func (p *ProgressBar) Draw(screen tcell.Screen) { if !p.GetVisible() { return } p.Box.Draw(screen) p.Lock() defer p.Unlock() x, y, width, height := p.GetInnerRect() barSize := height maxLength := width if p.vertical { barSize = width maxLength = height } barLength := int(math.RoundToEven(float64(maxLength) * (float64(p.progress) / float64(p.max)))) if barLength > maxLength { barLength = maxLength } for i := 0; i < barSize; i++ { for j := 0; j < barLength; j++ { if p.vertical { screen.SetContent(x+i, y+(height-1-j), p.filledRune, nil, tcell.StyleDefault.Foreground(p.filledColor).Background(p.backgroundColor)) } else { screen.SetContent(x+j, y+i, p.filledRune, nil, tcell.StyleDefault.Foreground(p.filledColor).Background(p.backgroundColor)) } } for j := barLength; j < maxLength; j++ { if p.vertical { screen.SetContent(x+i, y+(height-1-j), p.emptyRune, nil, tcell.StyleDefault.Foreground(p.emptyColor).Background(p.backgroundColor)) } else { screen.SetContent(x+j, y+i, p.emptyRune, nil, tcell.StyleDefault.Foreground(p.emptyColor).Background(p.backgroundColor)) } } } }
package cview import "github.com/gdamore/tcell/v2" // Semigraphics provides an easy way to access unicode characters for drawing. // // Named like the unicode characters, 'Semigraphics'-prefix used if unicode block // isn't prefixed itself. const ( // Block: General Punctation U+2000-U+206F (http://unicode.org/charts/PDF/U2000.pdf) SemigraphicsHorizontalEllipsis rune = '\u2026' // … // Block: Box Drawing U+2500-U+257F (http://unicode.org/charts/PDF/U2500.pdf) BoxDrawingsLightHorizontal rune = '\u2500' // ─ BoxDrawingsHeavyHorizontal rune = '\u2501' // ━ BoxDrawingsLightVertical rune = '\u2502' // │ BoxDrawingsHeavyVertical rune = '\u2503' // ┃ BoxDrawingsLightTripleDashHorizontal rune = '\u2504' // ┄ BoxDrawingsHeavyTripleDashHorizontal rune = '\u2505' // ┅ BoxDrawingsLightTripleDashVertical rune = '\u2506' // ┆ BoxDrawingsHeavyTripleDashVertical rune = '\u2507' // ┇ BoxDrawingsLightQuadrupleDashHorizontal rune = '\u2508' // ┈ BoxDrawingsHeavyQuadrupleDashHorizontal rune = '\u2509' // ┉ BoxDrawingsLightQuadrupleDashVertical rune = '\u250a' // ┊ BoxDrawingsHeavyQuadrupleDashVertical rune = '\u250b' // ┋ BoxDrawingsLightDownAndRight rune = '\u250c' // ┌ BoxDrawingsDownLighAndRightHeavy rune = '\u250d' // ┍ BoxDrawingsDownHeavyAndRightLight rune = '\u250e' // ┎ BoxDrawingsHeavyDownAndRight rune = '\u250f' // ┏ BoxDrawingsLightDownAndLeft rune = '\u2510' // ┐ BoxDrawingsDownLighAndLeftHeavy rune = '\u2511' // ┑ BoxDrawingsDownHeavyAndLeftLight rune = '\u2512' // ┒ BoxDrawingsHeavyDownAndLeft rune = '\u2513' // ┓ BoxDrawingsLightUpAndRight rune = '\u2514' // └ BoxDrawingsUpLightAndRightHeavy rune = '\u2515' // ┕ BoxDrawingsUpHeavyAndRightLight rune = '\u2516' // ┖ BoxDrawingsHeavyUpAndRight rune = '\u2517' // ┗ BoxDrawingsLightUpAndLeft rune = '\u2518' // ┘ BoxDrawingsUpLightAndLeftHeavy rune = '\u2519' // ┙ BoxDrawingsUpHeavyAndLeftLight rune = '\u251a' // ┚ BoxDrawingsHeavyUpAndLeft rune = '\u251b' // ┛ BoxDrawingsLightVerticalAndRight rune = '\u251c' // ├ BoxDrawingsVerticalLightAndRightHeavy rune = '\u251d' // ┝ BoxDrawingsUpHeavyAndRightDownLight rune = '\u251e' // ┞ BoxDrawingsDownHeacyAndRightUpLight rune = '\u251f' // ┟ BoxDrawingsVerticalHeavyAndRightLight rune = '\u2520' // ┠ BoxDrawingsDownLightAnbdRightUpHeavy rune = '\u2521' // ┡ BoxDrawingsUpLightAndRightDownHeavy rune = '\u2522' // ┢ BoxDrawingsHeavyVerticalAndRight rune = '\u2523' // ┣ BoxDrawingsLightVerticalAndLeft rune = '\u2524' // ┤ BoxDrawingsVerticalLightAndLeftHeavy rune = '\u2525' // ┥ BoxDrawingsUpHeavyAndLeftDownLight rune = '\u2526' // ┦ BoxDrawingsDownHeavyAndLeftUpLight rune = '\u2527' // ┧ BoxDrawingsVerticalheavyAndLeftLight rune = '\u2528' // ┨ BoxDrawingsDownLightAndLeftUpHeavy rune = '\u2529' // ┨ BoxDrawingsUpLightAndLeftDownHeavy rune = '\u252a' // ┪ BoxDrawingsHeavyVerticalAndLeft rune = '\u252b' // ┫ BoxDrawingsLightDownAndHorizontal rune = '\u252c' // ┬ BoxDrawingsLeftHeavyAndRightDownLight rune = '\u252d' // ┭ BoxDrawingsRightHeavyAndLeftDownLight rune = '\u252e' // ┮ BoxDrawingsDownLightAndHorizontalHeavy rune = '\u252f' // ┯ BoxDrawingsDownHeavyAndHorizontalLight rune = '\u2530' // ┰ BoxDrawingsRightLightAndLeftDownHeavy rune = '\u2531' // ┱ BoxDrawingsLeftLightAndRightDownHeavy rune = '\u2532' // ┲ BoxDrawingsHeavyDownAndHorizontal rune = '\u2533' // ┳ BoxDrawingsLightUpAndHorizontal rune = '\u2534' // ┴ BoxDrawingsLeftHeavyAndRightUpLight rune = '\u2535' // ┵ BoxDrawingsRightHeavyAndLeftUpLight rune = '\u2536' // ┶ BoxDrawingsUpLightAndHorizontalHeavy rune = '\u2537' // ┷ BoxDrawingsUpHeavyAndHorizontalLight rune = '\u2538' // ┸ BoxDrawingsRightLightAndLeftUpHeavy rune = '\u2539' // ┹ BoxDrawingsLeftLightAndRightUpHeavy rune = '\u253a' // ┺ BoxDrawingsHeavyUpAndHorizontal rune = '\u253b' // ┻ BoxDrawingsLightVerticalAndHorizontal rune = '\u253c' // ┼ BoxDrawingsLeftHeavyAndRightVerticalLight rune = '\u253d' // ┽ BoxDrawingsRightHeavyAndLeftVerticalLight rune = '\u253e' // ┾ BoxDrawingsVerticalLightAndHorizontalHeavy rune = '\u253f' // ┿ BoxDrawingsUpHeavyAndDownHorizontalLight rune = '\u2540' // ╀ BoxDrawingsDownHeavyAndUpHorizontalLight rune = '\u2541' // ╁ BoxDrawingsVerticalHeavyAndHorizontalLight rune = '\u2542' // ╂ BoxDrawingsLeftUpHeavyAndRightDownLight rune = '\u2543' // ╃ BoxDrawingsRightUpHeavyAndLeftDownLight rune = '\u2544' // ╄ BoxDrawingsLeftDownHeavyAndRightUpLight rune = '\u2545' // ╅ BoxDrawingsRightDownHeavyAndLeftUpLight rune = '\u2546' // ╆ BoxDrawingsDownLightAndUpHorizontalHeavy rune = '\u2547' // ╇ BoxDrawingsUpLightAndDownHorizontalHeavy rune = '\u2548' // ╈ BoxDrawingsRightLightAndLeftVerticalHeavy rune = '\u2549' // ╉ BoxDrawingsLeftLightAndRightVerticalHeavy rune = '\u254a' // ╊ BoxDrawingsHeavyVerticalAndHorizontal rune = '\u254b' // ╋ BoxDrawingsLightDoubleDashHorizontal rune = '\u254c' // ╌ BoxDrawingsHeavyDoubleDashHorizontal rune = '\u254d' // ╍ BoxDrawingsLightDoubleDashVertical rune = '\u254e' // ╎ BoxDrawingsHeavyDoubleDashVertical rune = '\u254f' // ╏ BoxDrawingsDoubleHorizontal rune = '\u2550' // ═ BoxDrawingsDoubleVertical rune = '\u2551' // ║ BoxDrawingsDownSingleAndRightDouble rune = '\u2552' // ╒ BoxDrawingsDownDoubleAndRightSingle rune = '\u2553' // ╓ BoxDrawingsDoubleDownAndRight rune = '\u2554' // ╔ BoxDrawingsDownSingleAndLeftDouble rune = '\u2555' // ╕ BoxDrawingsDownDoubleAndLeftSingle rune = '\u2556' // ╖ BoxDrawingsDoubleDownAndLeft rune = '\u2557' // ╗ BoxDrawingsUpSingleAndRightDouble rune = '\u2558' // ╘ BoxDrawingsUpDoubleAndRightSingle rune = '\u2559' // ╙ BoxDrawingsDoubleUpAndRight rune = '\u255a' // ╚ BoxDrawingsUpSingleAndLeftDouble rune = '\u255b' // ╛ BoxDrawingsUpDobuleAndLeftSingle rune = '\u255c' // ╜ BoxDrawingsDoubleUpAndLeft rune = '\u255d' // ╝ BoxDrawingsVerticalSingleAndRightDouble rune = '\u255e' // ╞ BoxDrawingsVerticalDoubleAndRightSingle rune = '\u255f' // ╟ BoxDrawingsDoubleVerticalAndRight rune = '\u2560' // ╠ BoxDrawingsVerticalSingleAndLeftDouble rune = '\u2561' // ╡ BoxDrawingsVerticalDoubleAndLeftSingle rune = '\u2562' // ╢ BoxDrawingsDoubleVerticalAndLeft rune = '\u2563' // ╣ BoxDrawingsDownSingleAndHorizontalDouble rune = '\u2564' // ╤ BoxDrawingsDownDoubleAndHorizontalSingle rune = '\u2565' // ╥ BoxDrawingsDoubleDownAndHorizontal rune = '\u2566' // ╦ BoxDrawingsUpSingleAndHorizontalDouble rune = '\u2567' // ╧ BoxDrawingsUpDoubleAndHorizontalSingle rune = '\u2568' // ╨ BoxDrawingsDoubleUpAndHorizontal rune = '\u2569' // ╩ BoxDrawingsVerticalSingleAndHorizontalDouble rune = '\u256a' // ╪ BoxDrawingsVerticalDoubleAndHorizontalSingle rune = '\u256b' // ╫ BoxDrawingsDoubleVerticalAndHorizontal rune = '\u256c' // ╬ BoxDrawingsLightArcDownAndRight rune = '\u256d' // ╭ BoxDrawingsLightArcDownAndLeft rune = '\u256e' // ╮ BoxDrawingsLightArcUpAndLeft rune = '\u256f' // ╯ BoxDrawingsLightArcUpAndRight rune = '\u2570' // ╰ BoxDrawingsLightDiagonalUpperRightToLowerLeft rune = '\u2571' // ╱ BoxDrawingsLightDiagonalUpperLeftToLowerRight rune = '\u2572' // ╲ BoxDrawingsLightDiagonalCross rune = '\u2573' // ╳ BoxDrawingsLightLeft rune = '\u2574' // ╴ BoxDrawingsLightUp rune = '\u2575' // ╵ BoxDrawingsLightRight rune = '\u2576' // ╶ BoxDrawingsLightDown rune = '\u2577' // ╷ BoxDrawingsHeavyLeft rune = '\u2578' // ╸ BoxDrawingsHeavyUp rune = '\u2579' // ╹ BoxDrawingsHeavyRight rune = '\u257a' // ╺ BoxDrawingsHeavyDown rune = '\u257b' // ╻ BoxDrawingsLightLeftAndHeavyRight rune = '\u257c' // ╼ BoxDrawingsLightUpAndHeavyDown rune = '\u257d' // ╽ BoxDrawingsHeavyLeftAndLightRight rune = '\u257e' // ╾ BoxDrawingsHeavyUpAndLightDown rune = '\u257f' // ╿ ) // SemigraphicJoints is a map for joining semigraphic (or otherwise) runes. // So far only light lines are supported but if you want to change the border // styling you need to provide the joints, too. // The matching will be sorted ascending by rune value, so you don't need to // provide all rune combinations, // e.g. (─) + (│) = (┼) will also match (│) + (─) = (┼) var SemigraphicJoints = map[string]rune{ // (─) + (│) = (┼) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVertical}): BoxDrawingsLightVerticalAndHorizontal, // (─) + (┌) = (┬) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndRight}): BoxDrawingsLightDownAndHorizontal, // (─) + (┐) = (┬) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal, // (─) + (└) = (┴) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndRight}): BoxDrawingsLightUpAndHorizontal, // (─) + (┘) = (┴) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal, // (─) + (├) = (┼) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal, // (─) + (┤) = (┼) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, // (─) + (┬) = (┬) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal, // (─) + (┴) = (┴) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal, // (─) + (┼) = (┼) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (│) + (┌) = (├) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndRight}): BoxDrawingsLightVerticalAndRight, // (│) + (┐) = (┤) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightVerticalAndLeft, // (│) + (└) = (├) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight, // (│) + (┘) = (┤) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft, // (│) + (├) = (├) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight, // (│) + (┤) = (┤) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft, // (│) + (┬) = (┼) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (│) + (┴) = (┼) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (│) + (┼) = (┼) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┌) + (┐) = (┬) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal, // (┌) + (└) = (├) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight, // (┌) + (┘) = (┼) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndHorizontal, // (┌) + (├) = (├) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight, // (┌) + (┤) = (┼) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, // (┌) + (┬) = (┬) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal, // (┌) + (┴) = (┼) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┌) + (┴) = (┼) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┐) + (└) = (┼) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndHorizontal, // (┐) + (┘) = (┤) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft, // (┐) + (├) = (┼) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal, // (┐) + (┤) = (┤) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft, // (┐) + (┬) = (┬) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal, // (┐) + (┴) = (┼) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┐) + (┼) = (┼) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (└) + (┘) = (┴) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal, // (└) + (├) = (├) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight, // (└) + (┤) = (┼) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, // (└) + (┬) = (┼) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (└) + (┴) = (┴) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal, // (└) + (┼) = (┼) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┘) + (├) = (┼) string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal, // (┘) + (┤) = (┤) string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft, // (┘) + (┬) = (┼) string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┘) + (┴) = (┴) string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal, // (┘) + (┼) = (┼) string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (├) + (┤) = (┼) string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, // (├) + (┬) = (┼) string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (├) + (┴) = (┼) string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (├) + (┼) = (┼) string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┤) + (┬) = (┼) string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┤) + (┴) = (┼) string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┤) + (┼) = (┼) string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┬) + (┴) = (┼) string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┬) + (┼) = (┼) string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┴) + (┼) = (┼) string([]rune{BoxDrawingsLightUpAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, } // PrintJoinedSemigraphics prints a semigraphics rune into the screen at the given // position with the given color, joining it with any existing semigraphics // rune. Background colors are preserved. At this point, only regular single // line borders are supported. func PrintJoinedSemigraphics(screen tcell.Screen, x, y int, ch rune, color tcell.Color) { previous, _, style, _ := screen.GetContent(x, y) style = style.Foreground(color) // What's the resulting rune? var result rune if ch == previous { result = ch } else { if ch < previous { previous, ch = ch, previous } result = SemigraphicJoints[string([]rune{previous, ch})] } if result == 0 { result = ch } // We only print something if we have something. screen.SetContent(x, y, result, nil, style) }
package cview import ( "math" "sync" "github.com/gdamore/tcell/v2" ) // Slider is a progress bar which may be modified via keyboard and mouse. type Slider struct { *ProgressBar // The text to be displayed before the slider. label []byte // The screen width of the label area. A value of 0 means use the width of // the label text. labelWidth int // The label color. labelColor tcell.Color // The label color when focused. labelColorFocused tcell.Color // The background color of the input area. fieldBackgroundColor tcell.Color // The background color of the input area when focused. fieldBackgroundColorFocused tcell.Color // The text color of the input area. fieldTextColor tcell.Color // The text color of the input area when focused. fieldTextColorFocused tcell.Color // The amount to increment by when modified via keyboard. increment int // Set to true when mouse dragging is in progress. dragging bool // An optional function which is called when the user changes the value of // this slider. changed func(value int) // An optional function which is called when the user indicated that they // are done entering text. The key which was pressed is provided (tab, // shift-tab, or escape). done func(tcell.Key) // A callback function set by the Form class and called when the user leaves // this form item. finished func(tcell.Key) sync.RWMutex } // NewSlider returns a new slider. func NewSlider() *Slider { s := &Slider{ ProgressBar: NewProgressBar(), increment: 10, labelColor: Styles.SecondaryTextColor, fieldBackgroundColor: Styles.ContrastBackgroundColor, fieldTextColor: Styles.PrimaryTextColor, labelColorFocused: ColorUnset, fieldBackgroundColorFocused: ColorUnset, fieldTextColorFocused: ColorUnset, } return s } // SetLabel sets the text to be displayed before the input area. func (s *Slider) SetLabel(label string) { s.Lock() defer s.Unlock() s.label = []byte(label) } // GetLabel returns the text to be displayed before the input area. func (s *Slider) GetLabel() string { s.RLock() defer s.RUnlock() return string(s.label) } // SetLabelWidth sets the screen width of the label. A value of 0 will cause the // primitive to use the width of the label string. func (s *Slider) SetLabelWidth(width int) { s.Lock() defer s.Unlock() s.labelWidth = width } // SetLabelColor sets the color of the label. func (s *Slider) SetLabelColor(color tcell.Color) { s.Lock() defer s.Unlock() s.labelColor = color } // SetLabelColorFocused sets the color of the label when focused. func (s *Slider) SetLabelColorFocused(color tcell.Color) { s.Lock() defer s.Unlock() s.labelColorFocused = color } // SetFieldBackgroundColor sets the background color of the input area. func (s *Slider) SetFieldBackgroundColor(color tcell.Color) { s.Lock() defer s.Unlock() s.fieldBackgroundColor = color } // SetFieldBackgroundColorFocused sets the background color of the input area when focused. func (s *Slider) SetFieldBackgroundColorFocused(color tcell.Color) { s.Lock() defer s.Unlock() s.fieldBackgroundColorFocused = color } // SetFieldTextColor sets the text color of the input area. func (s *Slider) SetFieldTextColor(color tcell.Color) { s.Lock() defer s.Unlock() s.fieldTextColor = color } // SetFieldTextColorFocused sets the text color of the input area when focused. func (s *Slider) SetFieldTextColorFocused(color tcell.Color) { s.Lock() defer s.Unlock() s.fieldTextColorFocused = color } // GetFieldHeight returns the height of the field. func (s *Slider) GetFieldHeight() int { return 1 } // GetFieldWidth returns this primitive's field width. func (s *Slider) GetFieldWidth() int { return 0 } // SetIncrement sets the amount the slider is incremented by when modified via // keyboard. func (s *Slider) SetIncrement(increment int) { s.Lock() defer s.Unlock() s.increment = increment } // SetChangedFunc sets a handler which is called when the value of this slider // was changed by the user. The handler function receives the new value. func (s *Slider) SetChangedFunc(handler func(value int)) { s.Lock() defer s.Unlock() s.changed = handler } // SetDoneFunc sets a handler which is called when the user is done using the // slider. The callback function is provided with the key that was pressed, // which is one of the following: // // - KeyEscape: Abort text input. // - KeyTab: Move to the next field. // - KeyBacktab: Move to the previous field. func (s *Slider) SetDoneFunc(handler func(key tcell.Key)) { s.Lock() defer s.Unlock() s.done = handler } // SetFinishedFunc sets a callback invoked when the user leaves this form item. func (s *Slider) SetFinishedFunc(handler func(key tcell.Key)) { s.Lock() defer s.Unlock() s.finished = handler } // Draw draws this primitive onto the screen. func (s *Slider) Draw(screen tcell.Screen) { if !s.GetVisible() { return } s.Box.Draw(screen) hasFocus := s.GetFocusable().HasFocus() s.Lock() // Select colors labelColor := s.labelColor fieldBackgroundColor := s.fieldBackgroundColor fieldTextColor := s.fieldTextColor if hasFocus { if s.labelColorFocused != ColorUnset { labelColor = s.labelColorFocused } if s.fieldBackgroundColorFocused != ColorUnset { fieldBackgroundColor = s.fieldBackgroundColorFocused } if s.fieldTextColorFocused != ColorUnset { fieldTextColor = s.fieldTextColorFocused } } // Prepare. x, y, width, height := s.GetInnerRect() rightLimit := x + width if height < 1 || rightLimit <= x { s.Unlock() return } // Draw label. if len(s.label) > 0 { if s.vertical { height-- // TODO draw label on bottom } else { if s.labelWidth > 0 { labelWidth := s.labelWidth if labelWidth > rightLimit-x { labelWidth = rightLimit - x } Print(screen, []byte(s.label), x, y, labelWidth, AlignLeft, labelColor) x += labelWidth + 1 width -= labelWidth + 1 } else { _, drawnWidth := Print(screen, []byte(s.label), x, y, rightLimit-x, AlignLeft, labelColor) x += drawnWidth + 1 width -= drawnWidth + 1 } } } // Draw slider. s.Unlock() s.ProgressBar.SetRect(x, y, width, height) s.ProgressBar.SetEmptyColor(fieldBackgroundColor) s.ProgressBar.SetFilledColor(fieldTextColor) s.ProgressBar.Draw(screen) } // InputHandler returns the handler for this primitive. func (s *Slider) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return s.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { if HitShortcut(event, Keys.Cancel, Keys.MovePreviousField, Keys.MoveNextField) { if s.done != nil { s.done(event.Key()) } if s.finished != nil { s.finished(event.Key()) } return } previous := s.progress if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) { s.SetProgress(0) } else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) { s.SetProgress(s.max) } else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MoveRight, Keys.MoveRight2, Keys.MovePreviousField) { s.AddProgress(s.increment) } else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveLeft, Keys.MoveLeft2, Keys.MoveNextField) { s.AddProgress(s.increment * -1) } if s.progress != previous && s.changed != nil { s.changed(s.progress) } }) } // MouseHandler returns the mouse handler for this primitive. func (s *Slider) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return s.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { x, y := event.Position() if !s.InRect(x, y) { s.dragging = false return false, nil } // Process mouse event. if action == MouseLeftClick { setFocus(s) consumed = true } handleMouse := func() { if !s.ProgressBar.InRect(x, y) { s.dragging = false return } bx, by, bw, bh := s.GetInnerRect() var clickPos, clickRange int if s.ProgressBar.vertical { clickPos = (bh - 1) - (y - by) clickRange = bh - 1 } else { clickPos = x - bx clickRange = bw - 1 } setValue := int(math.Floor(float64(s.max) * (float64(clickPos) / float64(clickRange)))) if setValue != s.progress { s.SetProgress(setValue) if s.changed != nil { s.changed(s.progress) } } } // Handle dragging. Clicks are implicitly handled by this logic. switch action { case MouseLeftDown: setFocus(s) consumed = true capture = s s.dragging = true handleMouse() case MouseMove: if s.dragging { consumed = true capture = s handleMouse() } case MouseLeftUp: if s.dragging { consumed = true s.dragging = false handleMouse() } } return }) }
package cview import ( "bytes" "fmt" "sync" "github.com/gdamore/tcell/v2" ) // TabbedPanels is a tabbed container for other primitives. The tab switcher // may be positioned vertically or horizontally, before or after the content. type TabbedPanels struct { *Flex Switcher *TextView panels *Panels tabLabels map[string]string currentTab string tabTextColor tcell.Color tabTextColorFocused tcell.Color tabBackgroundColor tcell.Color tabBackgroundColorFocused tcell.Color dividerStart string dividerMid string dividerEnd string switcherVertical bool switcherAfterContent bool width, lastWidth int setFocus func(Primitive) sync.RWMutex } // NewTabbedPanels returns a new TabbedPanels object. func NewTabbedPanels() *TabbedPanels { t := &TabbedPanels{ Flex: NewFlex(), Switcher: NewTextView(), panels: NewPanels(), tabTextColor: Styles.PrimaryTextColor, tabTextColorFocused: Styles.InverseTextColor, tabBackgroundColor: ColorUnset, tabBackgroundColorFocused: Styles.PrimaryTextColor, dividerMid: string(BoxDrawingsDoubleVertical), dividerEnd: string(BoxDrawingsLightVertical), tabLabels: make(map[string]string), } s := t.Switcher s.SetDynamicColors(true) s.SetRegions(true) s.SetWrap(true) s.SetWordWrap(true) s.SetHighlightedFunc(func(added, removed, remaining []string) { if len(added) == 0 { return } t.SetCurrentTab(added[0]) if t.setFocus != nil { t.setFocus(t.panels) } s.Highlight() }) t.rebuild() return t } // AddTab adds a new tab. Tab names should consist only of letters, numbers // and spaces. func (t *TabbedPanels) AddTab(name, label string, item Primitive) { t.Lock() t.tabLabels[name] = label t.Unlock() t.panels.AddPanel(name, item, true, false) t.updateAll() } // RemoveTab removes a tab. func (t *TabbedPanels) RemoveTab(name string) { t.panels.RemovePanel(name) t.updateAll() } // HasTab returns true if a tab with the given name exists in this object. func (t *TabbedPanels) HasTab(name string) bool { t.RLock() defer t.RUnlock() for _, panel := range t.panels.panels { if panel.Name == name { return true } } return false } // SetCurrentTab sets the currently visible tab. func (t *TabbedPanels) SetCurrentTab(name string) { t.Lock() if t.currentTab == name { t.Unlock() return } t.currentTab = name t.updateAll() t.Unlock() t.Switcher.Highlight(t.currentTab) } // GetCurrentTab returns the currently visible tab. func (t *TabbedPanels) GetCurrentTab() string { t.RLock() defer t.RUnlock() return t.currentTab } // SetTabLabel sets the label of a tab. func (t *TabbedPanels) SetTabLabel(name, label string) { t.Lock() defer t.Unlock() if t.tabLabels[name] == label { return } t.tabLabels[name] = label t.updateTabLabels() } // SetTabTextColor sets the color of the tab text. func (t *TabbedPanels) SetTabTextColor(color tcell.Color) { t.Lock() defer t.Unlock() t.tabTextColor = color } // SetTabTextColorFocused sets the color of the tab text when the tab is in focus. func (t *TabbedPanels) SetTabTextColorFocused(color tcell.Color) { t.Lock() defer t.Unlock() t.tabTextColorFocused = color } // SetTabBackgroundColor sets the background color of the tab. func (t *TabbedPanels) SetTabBackgroundColor(color tcell.Color) { t.Lock() defer t.Unlock() t.tabBackgroundColor = color } // SetTabBackgroundColorFocused sets the background color of the tab when the // tab is in focus. func (t *TabbedPanels) SetTabBackgroundColorFocused(color tcell.Color) { t.Lock() defer t.Unlock() t.tabBackgroundColorFocused = color } // SetTabSwitcherDivider sets the tab switcher divider text. Color tags are supported. func (t *TabbedPanels) SetTabSwitcherDivider(start, mid, end string) { t.Lock() defer t.Unlock() t.dividerStart, t.dividerMid, t.dividerEnd = start, mid, end } // SetTabSwitcherVertical sets the orientation of the tab switcher. func (t *TabbedPanels) SetTabSwitcherVertical(vertical bool) { t.Lock() defer t.Unlock() if t.switcherVertical == vertical { return } t.switcherVertical = vertical t.rebuild() } // SetTabSwitcherAfterContent sets whether the tab switcher is positioned after content. func (t *TabbedPanels) SetTabSwitcherAfterContent(after bool) { t.Lock() defer t.Unlock() if t.switcherAfterContent == after { return } t.switcherAfterContent = after t.rebuild() } func (t *TabbedPanels) rebuild() { f := t.Flex if t.switcherVertical { f.SetDirection(FlexColumn) } else { f.SetDirection(FlexRow) } f.RemoveItem(t.panels) f.RemoveItem(t.Switcher) if t.switcherAfterContent { f.AddItem(t.panels, 0, 1, true) f.AddItem(t.Switcher, 1, 1, false) } else { f.AddItem(t.Switcher, 1, 1, false) f.AddItem(t.panels, 0, 1, true) } t.updateTabLabels() } func (t *TabbedPanels) updateTabLabels() { if len(t.panels.panels) == 0 { t.Switcher.SetText("") t.Flex.ResizeItem(t.Switcher, 0, 1) return } maxWidth := 0 for _, panel := range t.panels.panels { label := t.tabLabels[panel.Name] if len(label) > maxWidth { maxWidth = len(label) } } var b bytes.Buffer if !t.switcherVertical { b.WriteString(t.dividerStart) } l := len(t.panels.panels) spacer := []byte(" ") for i, panel := range t.panels.panels { if i > 0 && t.switcherVertical { b.WriteRune('\n') } if t.switcherVertical && t.switcherAfterContent { b.WriteString(t.dividerMid) b.WriteRune(' ') } textColor := t.tabTextColor backgroundColor := t.tabBackgroundColor if panel.Name == t.currentTab { textColor = t.tabTextColorFocused backgroundColor = t.tabBackgroundColorFocused } label := t.tabLabels[panel.Name] if !t.switcherVertical { label = " " + label } if t.switcherVertical { spacer = bytes.Repeat([]byte(" "), maxWidth-len(label)+1) } b.WriteString(fmt.Sprintf(`["%s"][%s:%s]%s%s[-:-][""]`, panel.Name, ColorHex(textColor), ColorHex(backgroundColor), label, spacer)) if i == l-1 && !t.switcherVertical { b.WriteString(t.dividerEnd) } else if !t.switcherAfterContent { b.WriteString(t.dividerMid) } } t.Switcher.SetText(b.String()) var reqLines int if t.switcherVertical { reqLines = maxWidth + 2 } else { reqLines = len(WordWrap(t.Switcher.GetText(true), t.width)) if reqLines < 1 { reqLines = 1 } } t.Flex.ResizeItem(t.Switcher, reqLines, 1) } func (t *TabbedPanels) updateVisibleTabs() { allPanels := t.panels.panels var newTab string var foundCurrent bool for _, panel := range allPanels { if panel.Name == t.currentTab { newTab = panel.Name foundCurrent = true break } } if !foundCurrent { for _, panel := range allPanels { if panel.Name != "" { newTab = panel.Name break } } } if t.currentTab != newTab { t.SetCurrentTab(newTab) return } for _, panel := range allPanels { if panel.Name == t.currentTab { t.panels.ShowPanel(panel.Name) } else { t.panels.HidePanel(panel.Name) } } } func (t *TabbedPanels) updateAll() { t.updateTabLabels() t.updateVisibleTabs() } // Draw draws this primitive onto the screen. func (t *TabbedPanels) Draw(screen tcell.Screen) { if !t.GetVisible() { return } t.Box.Draw(screen) _, _, t.width, _ = t.GetInnerRect() if t.width != t.lastWidth { t.updateTabLabels() } t.lastWidth = t.width t.Flex.Draw(screen) } // InputHandler returns the handler for this primitive. func (t *TabbedPanels) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { if t.setFocus == nil { t.setFocus = setFocus } t.Flex.InputHandler()(event, setFocus) }) } // MouseHandler returns the mouse handler for this primitive. func (t *TabbedPanels) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if t.setFocus == nil { t.setFocus = setFocus } x, y := event.Position() if !t.InRect(x, y) { return false, nil } if t.Switcher.InRect(x, y) { if t.setFocus != nil { defer t.setFocus(t.panels) } defer t.Switcher.MouseHandler()(action, event, setFocus) return true, nil } return t.Flex.MouseHandler()(action, event, setFocus) }) }
package cview import ( "bytes" "sort" "sync" "github.com/gdamore/tcell/v2" colorful "github.com/lucasb-eyer/go-colorful" ) // TableCell represents one cell inside a Table. You can instantiate this type // directly but all colors (background and text) will be set to their default // which is black. type TableCell struct { // The reference object. Reference interface{} // The text to be displayed in the table cell. Text []byte // The alignment of the cell text. One of AlignLeft (default), AlignCenter, // or AlignRight. Align int // The maximum width of the cell in screen space. This is used to give a // column a maximum width. Any cell text whose screen width exceeds this width // is cut off. Set to 0 if there is no maximum width. MaxWidth int // If the total table width is less than the available width, this value is // used to add extra width to a column. See SetExpansion() for details. Expansion int // The color of the cell text. Color tcell.Color // The background color of the cell. BackgroundColor tcell.Color // The style attributes of the cell. Attributes tcell.AttrMask // If set to true, this cell cannot be selected. NotSelectable bool // The position and width of the cell the last time table was drawn. x, y, width int sync.RWMutex } // NewTableCell returns a new table cell with sensible defaults. That is, left // aligned text with the primary text color (see Styles) and a transparent // background (using the background of the Table). func NewTableCell(text string) *TableCell { return &TableCell{ Text: []byte(text), Align: AlignLeft, Color: Styles.PrimaryTextColor, BackgroundColor: tcell.ColorDefault, } } // SetBytes sets the cell's text. func (c *TableCell) SetBytes(text []byte) { c.Lock() defer c.Unlock() c.Text = text } // SetText sets the cell's text. func (c *TableCell) SetText(text string) { c.SetBytes([]byte(text)) } // GetBytes returns the cell's text. func (c *TableCell) GetBytes() []byte { c.RLock() defer c.RUnlock() return c.Text } // GetText returns the cell's text. func (c *TableCell) GetText() string { return string(c.GetBytes()) } // SetAlign sets the cell's text alignment, one of AlignLeft, AlignCenter, or // AlignRight. func (c *TableCell) SetAlign(align int) { c.Lock() defer c.Unlock() c.Align = align } // SetMaxWidth sets maximum width of the cell in screen space. This is used to // give a column a maximum width. Any cell text whose screen width exceeds this // width is cut off. Set to 0 if there is no maximum width. func (c *TableCell) SetMaxWidth(maxWidth int) { c.Lock() defer c.Unlock() c.MaxWidth = maxWidth } // SetExpansion sets the value by which the column of this cell expands if the // available width for the table is more than the table width (prior to applying // this expansion value). This is a proportional value. The amount of unused // horizontal space is divided into widths to be added to each column. How much // extra width a column receives depends on the expansion value: A value of 0 // (the default) will not cause the column to increase in width. Other values // are proportional, e.g. a value of 2 will cause a column to grow by twice // the amount of a column with a value of 1. // // Since this value affects an entire column, the maximum over all visible cells // in that column is used. // // This function panics if a negative value is provided. func (c *TableCell) SetExpansion(expansion int) { c.Lock() defer c.Unlock() if expansion < 0 { panic("Table cell expansion values may not be negative") } c.Expansion = expansion } // SetTextColor sets the cell's text color. func (c *TableCell) SetTextColor(color tcell.Color) { c.Lock() defer c.Unlock() c.Color = color } // SetBackgroundColor sets the cell's background color. Set to // tcell.ColorDefault to use the table's background color. func (c *TableCell) SetBackgroundColor(color tcell.Color) { c.Lock() defer c.Unlock() c.BackgroundColor = color } // SetAttributes sets the cell's text attributes. You can combine different // attributes using bitmask operations: // // cell.SetAttributes(tcell.AttrUnderline | tcell.AttrBold) func (c *TableCell) SetAttributes(attr tcell.AttrMask) { c.Lock() defer c.Unlock() c.Attributes = attr } // SetStyle sets the cell's style (foreground color, background color, and // attributes) all at once. func (c *TableCell) SetStyle(style tcell.Style) { c.Lock() defer c.Unlock() c.Color, c.BackgroundColor, c.Attributes = style.Decompose() } // SetSelectable sets whether or not this cell can be selected by the user. func (c *TableCell) SetSelectable(selectable bool) { c.Lock() defer c.Unlock() c.NotSelectable = !selectable } // SetReference allows you to store a reference of any type in this cell. This // will allow you to establish a mapping between the cell and your // actual data. func (c *TableCell) SetReference(reference interface{}) { c.Lock() defer c.Unlock() c.Reference = reference } // GetReference returns this cell's reference object. func (c *TableCell) GetReference() interface{} { c.RLock() defer c.RUnlock() return c.Reference } // GetLastPosition returns the position of the table cell the last time it was // drawn on screen. If the cell is not on screen, the return values are // undefined. // // Because the Table class will attempt to keep selected cells on screen, this // function is most useful in response to a "selected" event (see // SetSelectedFunc()) or a "selectionChanged" event (see // SetSelectionChangedFunc()). func (c *TableCell) GetLastPosition() (x, y, width int) { c.RLock() defer c.RUnlock() return c.x, c.y, c.width } // Table visualizes two-dimensional data consisting of rows and columns. Each // Table cell is defined via SetCell() by the TableCell type. They can be added // dynamically to the table and changed any time. // // Each row of the table must have the same number of columns when it is drawn // or navigated. This isn't strictly enforced, however you may encounter issues // when navigating a table with rows of varied column sizes. // // The most compact display of a table is without borders. Each row will then // occupy one row on screen and columns are separated by the rune defined via // SetSeparator() (a space character by default). // // When borders are turned on (via SetBorders()), each table cell is surrounded // by lines. Therefore one table row will require two rows on screen. // // Columns will use as much horizontal space as they need. You can constrain // their size with the MaxWidth parameter of the TableCell type. // // Fixed Columns // // You can define fixed rows and rolumns via SetFixed(). They will always stay // in their place, even when the table is scrolled. Fixed rows are always the // top rows. Fixed columns are always the leftmost columns. // // Selections // // You can call SetSelectable() to set columns and/or rows to "selectable". If // the flag is set only for columns, entire columns can be selected by the user. // If it is set only for rows, entire rows can be selected. If both flags are // set, individual cells can be selected. The "selected" handler set via // SetSelectedFunc() is invoked when the user presses Enter on a selection. // // Navigation // // If the table extends beyond the available space, it can be navigated with // key bindings similar to Vim: // // - h, left arrow: Move left by one column. // - l, right arrow: Move right by one column. // - j, down arrow: Move down by one row. // - k, up arrow: Move up by one row. // - g, home: Move to the top. // - G, end: Move to the bottom. // - Ctrl-F, page down: Move down by one page. // - Ctrl-B, page up: Move up by one page. // // When there is no selection, this affects the entire table (except for fixed // rows and columns). When there is a selection, the user moves the selection. // The class will attempt to keep the selection from moving out of the screen. // // Use SetInputCapture() to override or modify keyboard input. type Table struct { *Box // Whether or not this table has borders around each cell. cellBorders bool // The color of the cell borders or the separator. cellBorderColor tcell.Color // If there are no borders, the column separator. separator rune // The size of the padding between each cell rowPadding, columnPadding int // The cells of the table. Rows first, then columns. cells [][]*TableCell // The rightmost column in the data set. lastColumn int // If true, when calculating the widths of the columns, all rows are evaluated // instead of only the visible ones. evaluateAllRows bool // The number of fixed rows / columns. fixedRows, fixedColumns int // Whether or not rows or columns can be selected. If both are set to true, // cells can be selected. rowsSelectable, columnsSelectable bool // The currently selected row and column. selectedRow, selectedColumn int // The number of rows/columns by which the table is scrolled down/to the // right. rowOffset, columnOffset int // If set to true, the table's last row will always be visible. trackEnd bool // The sort function of the table. Defaults to a case-sensitive comparison. sortFunc func(column, i, j int) bool // Whether or not the table should be sorted when a fixed row is clicked. sortClicked bool // The last direction the table was sorted by when clicked. sortClickedDescending bool // The last column the table was sorted by when clicked. sortClickedColumn int // The number of visible rows the last time the table was drawn. visibleRows int // The indices of the visible columns as of the last time the table was drawn. visibleColumnIndices []int // The net widths of the visible columns as of the last time the table was // drawn. visibleColumnWidths []int // Visibility of the scroll bar. scrollBarVisibility ScrollBarVisibility // The scroll bar color. scrollBarColor tcell.Color // The style of the selected rows. If this value is StyleDefault, selected rows // are simply inverted. selectedStyle tcell.Style // An optional function which gets called when the user presses Enter on a // selected cell. If entire rows selected, the column value is undefined. // Likewise for entire columns. selected func(row, column int) // An optional function which gets called when the user changes the selection. // If entire rows selected, the column value is undefined. // Likewise for entire columns. selectionChanged func(row, column int) // An optional function which gets called when the user presses Escape, Tab, // or Backtab. Also when the user presses Enter if nothing is selectable. done func(key tcell.Key) sync.RWMutex } // NewTable returns a new table. func NewTable() *Table { return &Table{ Box: NewBox(), scrollBarVisibility: ScrollBarAuto, scrollBarColor: Styles.ScrollBarColor, cellBorderColor: Styles.GraphicsColor, separator: ' ', sortClicked: true, lastColumn: -1, } } // Clear removes all table data. func (t *Table) Clear() { t.Lock() defer t.Unlock() t.cells = nil t.lastColumn = -1 } // SetCellBorders sets whether or not each cell in the table is surrounded by a // border. func (t *Table) SetCellBorders(show bool) { t.Lock() defer t.Unlock() t.cellBorders = show } // SetCellBorderColor sets the color of the cell borders. func (t *Table) SetCellBorderColor(color tcell.Color) { t.Lock() defer t.Unlock() t.cellBorderColor = color } // SetCellPadding sets the size of the padding between each cell. func (t *Table) SetCellPadding(row, column int) { t.Lock() defer t.Unlock() if t.rowPadding < 0 { t.rowPadding = 0 } if t.columnPadding < 0 { t.columnPadding = 0 } t.rowPadding, t.columnPadding = row, column } // SetScrollBarVisibility specifies the display of the scroll bar. func (t *Table) SetScrollBarVisibility(visibility ScrollBarVisibility) { t.Lock() defer t.Unlock() t.scrollBarVisibility = visibility } // SetScrollBarColor sets the color of the scroll bar. func (t *Table) SetScrollBarColor(color tcell.Color) { t.Lock() defer t.Unlock() t.scrollBarColor = color } // SetSelectedStyle sets a specific style for selected cells. If no such style // is set, per default, selected cells are inverted (i.e. their foreground and // background colors are swapped). // // To reset a previous setting to its default, make the following call: // // table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, 0) func (t *Table) SetSelectedStyle(foregroundColor, backgroundColor tcell.Color, attributes tcell.AttrMask) { t.Lock() defer t.Unlock() t.selectedStyle = SetAttributes(tcell.StyleDefault.Foreground(foregroundColor).Background(backgroundColor), attributes) } // SetSeparator sets the character used to fill the space between two // neighboring cells. This is a space character ' ' per default but you may // want to set it to Borders.Vertical (or any other rune) if the column // separation should be more visible. If cell borders are activated, this is // ignored. // // Separators have the same color as borders. func (t *Table) SetSeparator(separator rune) { t.Lock() defer t.Unlock() t.separator = separator } // SetFixed sets the number of fixed rows and columns which are always visible // even when the rest of the cells are scrolled out of view. Rows are always the // top-most ones. Columns are always the left-most ones. func (t *Table) SetFixed(rows, columns int) { t.Lock() defer t.Unlock() t.fixedRows, t.fixedColumns = rows, columns } // SetSelectable sets the flags which determine what can be selected in a table. // There are three selection modi: // // - rows = false, columns = false: Nothing can be selected. // - rows = true, columns = false: Rows can be selected. // - rows = false, columns = true: Columns can be selected. // - rows = true, columns = true: Individual cells can be selected. func (t *Table) SetSelectable(rows, columns bool) { t.Lock() defer t.Unlock() t.rowsSelectable, t.columnsSelectable = rows, columns } // GetSelectable returns what can be selected in a table. Refer to // SetSelectable() for details. func (t *Table) GetSelectable() (rows, columns bool) { t.RLock() defer t.RUnlock() return t.rowsSelectable, t.columnsSelectable } // GetSelection returns the position of the current selection. // If entire rows are selected, the column index is undefined. // Likewise for entire columns. func (t *Table) GetSelection() (row, column int) { t.RLock() defer t.RUnlock() return t.selectedRow, t.selectedColumn } // Select sets the selected cell. Depending on the selection settings // specified via SetSelectable(), this may be an entire row or column, or even // ignored completely. The "selection changed" event is fired if such a callback // is available (even if the selection ends up being the same as before and even // if cells are not selectable). func (t *Table) Select(row, column int) { t.Lock() defer t.Unlock() t.selectedRow, t.selectedColumn = row, column if t.selectionChanged != nil { t.Unlock() t.selectionChanged(row, column) t.Lock() } } // SetOffset sets how many rows and columns should be skipped when drawing the // table. This is useful for large tables that do not fit on the screen. // Navigating a selection can change these values. // // Fixed rows and columns are never skipped. func (t *Table) SetOffset(row, column int) { t.Lock() defer t.Unlock() t.rowOffset, t.columnOffset = row, column t.trackEnd = false } // GetOffset returns the current row and column offset. This indicates how many // rows and columns the table is scrolled down and to the right. func (t *Table) GetOffset() (row, column int) { t.RLock() defer t.RUnlock() return t.rowOffset, t.columnOffset } // SetEvaluateAllRows sets a flag which determines the rows to be evaluated when // calculating the widths of the table's columns. When false, only visible rows // are evaluated. When true, all rows in the table are evaluated. // // Set this flag to true to avoid shifting column widths when the table is // scrolled. (May be slower for large tables.) func (t *Table) SetEvaluateAllRows(all bool) { t.Lock() defer t.Unlock() t.evaluateAllRows = all } // SetSelectedFunc sets a handler which is called whenever the user presses the // Enter key on a selected cell/row/column. The handler receives the position of // the selection and its cell contents. If entire rows are selected, the column // index is undefined. Likewise for entire columns. func (t *Table) SetSelectedFunc(handler func(row, column int)) { t.Lock() defer t.Unlock() t.selected = handler } // SetSelectionChangedFunc sets a handler which is called whenever the current // selection changes. The handler receives the position of the new selection. // If entire rows are selected, the column index is undefined. Likewise for // entire columns. func (t *Table) SetSelectionChangedFunc(handler func(row, column int)) { t.Lock() defer t.Unlock() t.selectionChanged = handler } // SetDoneFunc sets a handler which is called whenever the user presses the // Escape, Tab, or Backtab key. If nothing is selected, it is also called when // user presses the Enter key (because pressing Enter on a selection triggers // the "selected" handler set via SetSelectedFunc()). func (t *Table) SetDoneFunc(handler func(key tcell.Key)) { t.Lock() defer t.Unlock() t.done = handler } // SetCell sets the content of a cell the specified position. It is ok to // directly instantiate a TableCell object. If the cell has content, at least // the Text and Color fields should be set. // // Note that setting cells in previously unknown rows and columns will // automatically extend the internal table representation, e.g. starting with // a row of 100,000 will immediately create 100,000 empty rows. // // To avoid unnecessary garbage collection, fill columns from left to right. func (t *Table) SetCell(row, column int, cell *TableCell) { t.Lock() defer t.Unlock() if row >= len(t.cells) { t.cells = append(t.cells, make([][]*TableCell, row-len(t.cells)+1)...) } rowLen := len(t.cells[row]) if column >= rowLen { t.cells[row] = append(t.cells[row], make([]*TableCell, column-rowLen+1)...) for c := rowLen; c < column; c++ { t.cells[row][c] = &TableCell{} } } t.cells[row][column] = cell if column > t.lastColumn { t.lastColumn = column } } // SetCellSimple calls SetCell() with the given text, left-aligned, in white. func (t *Table) SetCellSimple(row, column int, text string) { t.SetCell(row, column, NewTableCell(text)) } // GetCell returns the contents of the cell at the specified position. A valid // TableCell object is always returned but it will be uninitialized if the cell // was not previously set. Such an uninitialized object will not automatically // be inserted. Therefore, repeated calls to this function may return different // pointers for uninitialized cells. func (t *Table) GetCell(row, column int) *TableCell { t.RLock() defer t.RUnlock() if row >= len(t.cells) || column >= len(t.cells[row]) { return &TableCell{} } return t.cells[row][column] } // RemoveRow removes the row at the given position from the table. If there is // no such row, this has no effect. func (t *Table) RemoveRow(row int) { t.Lock() defer t.Unlock() if row < 0 || row >= len(t.cells) { return } t.cells = append(t.cells[:row], t.cells[row+1:]...) } // RemoveColumn removes the column at the given position from the table. If // there is no such column, this has no effect. func (t *Table) RemoveColumn(column int) { t.Lock() defer t.Unlock() for row := range t.cells { if column < 0 || column >= len(t.cells[row]) { continue } t.cells[row] = append(t.cells[row][:column], t.cells[row][column+1:]...) } } // InsertRow inserts a row before the row with the given index. Cells on the // given row and below will be shifted to the bottom by one row. If "row" is // equal or larger than the current number of rows, this function has no effect. func (t *Table) InsertRow(row int) { t.Lock() defer t.Unlock() if row >= len(t.cells) { return } t.cells = append(t.cells, nil) // Extend by one. copy(t.cells[row+1:], t.cells[row:]) // Shift down. t.cells[row] = nil // New row is uninitialized. } // InsertColumn inserts a column before the column with the given index. Cells // in the given column and to its right will be shifted to the right by one // column. Rows that have fewer initialized cells than "column" will remain // unchanged. func (t *Table) InsertColumn(column int) { t.Lock() defer t.Unlock() for row := range t.cells { if column >= len(t.cells[row]) { continue } t.cells[row] = append(t.cells[row], nil) // Extend by one. copy(t.cells[row][column+1:], t.cells[row][column:]) // Shift to the right. t.cells[row][column] = &TableCell{} // New element is an uninitialized table cell. } } // GetRowCount returns the number of rows in the table. func (t *Table) GetRowCount() int { t.RLock() defer t.RUnlock() return len(t.cells) } // GetColumnCount returns the (maximum) number of columns in the table. func (t *Table) GetColumnCount() int { t.RLock() defer t.RUnlock() if len(t.cells) == 0 { return 0 } return t.lastColumn + 1 } // cellAt returns the row and column located at the given screen coordinates. // Each returned value may be negative if there is no row and/or cell. This // function will also process coordinates outside the table's inner rectangle so // callers will need to check for bounds themselves. func (t *Table) cellAt(x, y int) (row, column int) { rectX, rectY, _, _ := t.GetInnerRect() // Determine row as seen on screen. if t.cellBorders { row = (y - rectY - 1) / 2 } else { row = y - rectY } // Respect fixed rows and row offset. if row >= 0 { if row >= t.fixedRows { row += t.rowOffset } if row >= len(t.cells) { row = -1 } } // Search for the clicked column. column = -1 if x >= rectX { columnX := rectX if t.cellBorders { columnX++ } for index, width := range t.visibleColumnWidths { columnX += width + 1 if x < columnX { column = t.visibleColumnIndices[index] break } } } return } // ScrollToBeginning scrolls the table to the beginning to that the top left // corner of the table is shown. Note that this position may be corrected if // there is a selection. func (t *Table) ScrollToBeginning() { t.Lock() defer t.Unlock() t.trackEnd = false t.columnOffset = 0 t.rowOffset = 0 } // ScrollToEnd scrolls the table to the beginning to that the bottom left corner // of the table is shown. Adding more rows to the table will cause it to // automatically scroll with the new data. Note that this position may be // corrected if there is a selection. func (t *Table) ScrollToEnd() { t.Lock() defer t.Unlock() t.trackEnd = true t.columnOffset = 0 t.rowOffset = len(t.cells) } // SetSortClicked sets a flag which determines whether the table is sorted when // a fixed row is clicked. This flag is enabled by default. func (t *Table) SetSortClicked(sortClicked bool) { t.Lock() defer t.Unlock() t.sortClicked = sortClicked } // SetSortFunc sets the sorting function used for the table. When unset, a // case-sensitive string comparison is used. func (t *Table) SetSortFunc(sortFunc func(column, i, j int) bool) { t.Lock() defer t.Unlock() t.sortFunc = sortFunc } // Sort sorts the table by the column at the given index. You may set a custom // sorting function with SetSortFunc. func (t *Table) Sort(column int, descending bool) { t.Lock() defer t.Unlock() if len(t.cells) == 0 || column < 0 || column >= len(t.cells[0]) { return } if t.sortFunc == nil { t.sortFunc = func(column, i, j int) bool { return bytes.Compare(t.cells[i][column].Text, t.cells[j][column].Text) == -1 } } sort.SliceStable(t.cells, func(i, j int) bool { if i < t.fixedRows { return i < j } else if j < t.fixedRows { return j > i } if !descending { return t.sortFunc(column, i, j) } return t.sortFunc(column, j, i) }) } // Draw draws this primitive onto the screen. func (t *Table) Draw(screen tcell.Screen) { if !t.GetVisible() { return } t.Box.Draw(screen) t.Lock() defer t.Unlock() // What's our available screen space? x, y, width, height := t.GetInnerRect() if t.cellBorders { t.visibleRows = height / 2 } else { t.visibleRows = height / (t.rowPadding + 1) } showVerticalScrollBar := t.scrollBarVisibility == ScrollBarAlways || (t.scrollBarVisibility == ScrollBarAuto && len(t.cells) > t.visibleRows-t.fixedRows) if showVerticalScrollBar { width-- // Subtract space for scroll bar. } // Return the cell at the specified position (nil if it doesn't exist). getCell := func(row, column int) *TableCell { if row < 0 || column < 0 || row >= len(t.cells) || column >= len(t.cells[row]) { return nil } return t.cells[row][column] } // If this cell is not selectable, find the next one. if t.rowsSelectable || t.columnsSelectable { if t.selectedColumn < 0 { t.selectedColumn = 0 } if t.selectedRow < 0 { t.selectedRow = 0 } for t.selectedRow < len(t.cells) { cell := getCell(t.selectedRow, t.selectedColumn) if cell == nil || !cell.NotSelectable { break } t.selectedColumn++ if t.selectedColumn > t.lastColumn { t.selectedColumn = 0 t.selectedRow++ } } } // Clamp row offsets. if t.rowsSelectable { if t.selectedRow >= t.fixedRows && t.selectedRow < t.fixedRows+t.rowOffset { t.rowOffset = t.selectedRow - t.fixedRows t.trackEnd = false } if t.cellBorders { if 2*(t.selectedRow+1-t.rowOffset) >= height { t.rowOffset = t.selectedRow + 1 - height/2 t.trackEnd = false } } else { if t.selectedRow-t.rowOffset >= height/(t.rowPadding+1) { t.rowOffset = t.selectedRow - height/(t.rowPadding+1) t.trackEnd = false } } } if t.cellBorders { if 2*(len(t.cells)-t.rowOffset) < height { t.trackEnd = true } } else { if len(t.cells)-t.rowOffset < height/(t.rowPadding+1) { t.trackEnd = true } } if t.trackEnd { if t.cellBorders { t.rowOffset = len(t.cells) - height/2 } else { t.rowOffset = len(t.cells) - height/(t.rowPadding+1) } } if t.rowOffset < 0 { t.rowOffset = 0 } // Clamp column offset. (Only left side here. The right side is more // difficult and we'll do it below.) if t.columnsSelectable && t.selectedColumn >= t.fixedColumns && t.selectedColumn < t.fixedColumns+t.columnOffset { t.columnOffset = t.selectedColumn - t.fixedColumns } if t.columnOffset < 0 { t.columnOffset = 0 } if t.selectedColumn < 0 { t.selectedColumn = 0 } // Determine the indices and widths of the columns and rows which fit on the // screen. var ( columns, rows, allRows, widths []int tableHeight, tableWidth int ) columnPadding := t.columnPadding if t.cellBorders { columnPadding++ } rowStep := t.rowPadding + 1 if t.cellBorders { rowStep++ // With borders, every table row takes two screen rows. tableWidth = 1 // We start at the second character because of the left table border. } if t.evaluateAllRows { allRows = make([]int, len(t.cells)) for row := range t.cells { allRows[row] = row } } indexRow := func(row int) bool { // Determine if this row is visible, store its index. if tableHeight >= height { return false } rows = append(rows, row) tableHeight += rowStep for i := 0; i < rowStep-1; i++ { rows = append(rows, -1) } return true } for row := 0; row < t.fixedRows && row < len(t.cells); row++ { // Do the fixed rows first. if !indexRow(row) { break } } for row := t.fixedRows + t.rowOffset; row < len(t.cells); row++ { // Then the remaining rows. if !indexRow(row) { break } } var ( skipped, lastTableWidth, expansionTotal int expansions []int ) ColumnLoop: for column := 0; ; column++ { // If we've moved beyond the right border, we stop or skip a column. for tableWidth-1 >= width { // -1 because we include one extra column if the separator falls on the right end of the box. // We've moved beyond the available space. if column < t.fixedColumns { break ColumnLoop // We're in the fixed area. We're done. } if !t.columnsSelectable && skipped >= t.columnOffset { break ColumnLoop // There is no selection and we've already reached the offset. } if t.columnsSelectable && t.selectedColumn-skipped == t.fixedColumns { break ColumnLoop // The selected column reached the leftmost point before disappearing. } if t.columnsSelectable && skipped >= t.columnOffset && (t.selectedColumn < column && lastTableWidth < width-1 && tableWidth < width-1 || t.selectedColumn < column-1) { break ColumnLoop // We've skipped as many as requested and the selection is visible. } if len(columns) <= t.fixedColumns { break // Nothing to skip. } // We need to skip a column. skipped++ lastTableWidth -= widths[t.fixedColumns] + 1 tableWidth -= widths[t.fixedColumns] + 1 columns = append(columns[:t.fixedColumns], columns[t.fixedColumns+1:]...) widths = append(widths[:t.fixedColumns], widths[t.fixedColumns+1:]...) expansions = append(expansions[:t.fixedColumns], expansions[t.fixedColumns+1:]...) } // What's this column's width (without expansion)? maxWidth := -1 expansion := 0 evaluationRows := rows if t.evaluateAllRows { evaluationRows = allRows } for _, row := range evaluationRows { if row == -1 { continue } if cell := getCell(row, column); cell != nil { _, _, _, _, _, _, cellWidth := decomposeText(cell.Text, true, false) if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth { cellWidth = cell.MaxWidth } if cellWidth > maxWidth { maxWidth = cellWidth } if cell.Expansion > expansion { expansion = cell.Expansion } } } if maxWidth < 0 { break // No more cells found in this column. } // Store new column info at the end. columns = append(columns, column) widths = append(widths, maxWidth) lastTableWidth = tableWidth tableWidth += maxWidth + columnPadding expansions = append(expansions, expansion) expansionTotal += expansion } t.columnOffset = skipped // If we have space left, distribute it. if tableWidth < width { toDistribute := width - tableWidth for index, expansion := range expansions { if expansionTotal <= 0 { break } expWidth := toDistribute * expansion / expansionTotal widths[index] += expWidth toDistribute -= expWidth expansionTotal -= expansion } tableWidth = width - toDistribute } // Helper function which draws border runes. borderStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.cellBorderColor) drawBorder := func(colX, rowY int, ch rune) { screen.SetContent(x+colX, y+rowY, ch, nil, borderStyle) } // Draw the cells (and borders). var columnX int if !t.cellBorders { columnX-- } for columnIndex, column := range columns { columnWidth := widths[columnIndex] for rowY, row := range rows { if t.cellBorders { // Draw borders. rowY *= 2 for pos := 0; pos < columnWidth+columnPadding && columnX+columnPadding+pos < width; pos++ { if row == -1 { continue } drawBorder(columnX+pos+1, rowY, Borders.Horizontal) } ch := Borders.Cross if columnIndex == 0 { if rowY == 0 { ch = Borders.TopLeft } else { ch = Borders.LeftT } } else if rowY == 0 { ch = Borders.TopT } if row == -1 { ch = Borders.Vertical } drawBorder(columnX, rowY, ch) rowY++ if rowY >= height { break // No space for the text anymore. } drawBorder(columnX, rowY, Borders.Vertical) } else if columnIndex > 0 && columnPadding != 0 { // Draw separator. drawBorder(columnX, rowY, t.separator) } // Get the cell. cell := getCell(row, column) if cell == nil { continue } // Draw text. finalWidth := columnWidth if columnX+columnPadding+columnWidth >= width { finalWidth = width - columnX - columnPadding } cell.x, cell.y, cell.width = x+columnX+columnPadding, y+rowY, finalWidth _, printed := PrintStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, SetAttributes(tcell.StyleDefault.Foreground(cell.Color), cell.Attributes)) if TaggedTextWidth(cell.Text)-printed > 0 && printed > 0 { _, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY) PrintStyle(screen, []byte(string(SemigraphicsHorizontalEllipsis)), x+columnX+finalWidth, y+rowY, t.rowPadding, AlignLeft, style) //PrintStyle(screen, []byte(string(SemigraphicsHorizontalEllipsis)), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style) } } // Draw bottom border. if rowY := 2 * len(rows); t.cellBorders && rowY < height { for pos := 0; pos < columnWidth+columnPadding && columnX+columnPadding+pos < width; pos++ { drawBorder(columnX+pos+1, rowY, Borders.Horizontal) } ch := Borders.BottomT if columnIndex == 0 { ch = Borders.BottomLeft } drawBorder(columnX, rowY, ch) } columnX += columnWidth + columnPadding } // Draw right border. if t.cellBorders && len(t.cells) > 0 && columnX < width { for rowY, row := range rows { rowY *= 2 if rowY+1 < height { drawBorder(columnX, rowY+1, Borders.Vertical) } ch := Borders.RightT if rowY == 0 { ch = Borders.TopRight } if row == -1 { ch = Borders.Vertical } drawBorder(columnX, rowY, ch) } if rowY := 2 * len(rows); rowY < height { drawBorder(columnX, rowY, Borders.BottomRight) } } if showVerticalScrollBar { // Calculate scroll bar position and dimensions. rows := len(t.cells) scrollBarItems := rows - t.fixedRows scrollBarHeight := (t.visibleRows*(t.rowPadding+1) - t.fixedRows) + 1 scrollBarX := x + width scrollBarY := y + t.fixedRows if scrollBarX > x+tableWidth { scrollBarX = x + tableWidth } padTotalOffset := 1 if t.cellBorders { padTotalOffset = 2 scrollBarItems *= 2 scrollBarHeight = (scrollBarHeight * 2) - 1 scrollBarY += t.fixedRows + 1 } // Draw scroll bar. extra := 3 if t.rowPadding == 0 { extra = 16 } cursor := int(float64(scrollBarItems+extra) * (float64(t.rowOffset) / float64(((rows-t.fixedRows)-t.visibleRows)+padTotalOffset))) for printed := 0; printed < scrollBarHeight; printed++ { RenderScrollBar(screen, t.scrollBarVisibility, scrollBarX, scrollBarY+printed, scrollBarHeight, scrollBarItems, cursor, printed, t.hasFocus, t.scrollBarColor) } } // TODO Draw horizontal scroll bar // Helper function which colors the background of a box. // backgroundColor == tcell.ColorDefault => Don't color the background. // textColor == tcell.ColorDefault => Don't change the text color. // attr == 0 => Don't change attributes. // invert == true => Ignore attr, set text to backgroundColor or t.backgroundColor; // set background to textColor. colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, attr tcell.AttrMask, invert bool) { for by := 0; by < h && fromY+by < y+height; by++ { for bx := 0; bx < w && fromX+bx < x+width; bx++ { m, c, style, _ := screen.GetContent(fromX+bx, fromY+by) fg, bg, a := style.Decompose() if invert { if fg == textColor || fg == t.cellBorderColor { fg = backgroundColor } if fg == tcell.ColorDefault { fg = t.backgroundColor } style = style.Background(textColor).Foreground(fg) } else { if backgroundColor != tcell.ColorDefault { bg = backgroundColor } if textColor != tcell.ColorDefault { fg = textColor } if attr != 0 { a = attr } style = SetAttributes(style.Background(bg).Foreground(fg), a) } screen.SetContent(fromX+bx, fromY+by, m, c, style) } } } // Color the cell backgrounds. To avoid undesirable artefacts, we combine // the drawing of a cell by background color, selected cells last. type cellInfo struct { x, y, w, h int color tcell.Color selected bool } cellsByBackgroundColor := make(map[tcell.Color][]*cellInfo) var backgroundColors []tcell.Color for rowY, row := range rows { columnX := 0 rowSelected := t.rowsSelectable && !t.columnsSelectable && row == t.selectedRow for columnIndex, column := range columns { columnWidth := widths[columnIndex] cell := getCell(row, column) if cell == nil { continue } bx, by, bw, bh := x+columnX, y+rowY, columnWidth+columnPadding, 1 if t.cellBorders { by = y + rowY*2 bw++ bh = 3 } columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn cellSelected := !cell.NotSelectable && (columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow) entries, ok := cellsByBackgroundColor[cell.BackgroundColor] cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &cellInfo{ x: bx, y: by, w: bw, h: bh, color: cell.Color, selected: cellSelected, }) if !ok { backgroundColors = append(backgroundColors, cell.BackgroundColor) } columnX += columnWidth + columnPadding } } sort.Slice(backgroundColors, func(i int, j int) bool { // Draw brightest colors last (i.e. on top). r, g, b := backgroundColors[i].RGB() c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255} _, _, li := c.Hcl() r, g, b = backgroundColors[j].RGB() c = colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255} _, _, lj := c.Hcl() return li < lj }) selFg, selBg, selAttr := t.selectedStyle.Decompose() for _, bgColor := range backgroundColors { entries := cellsByBackgroundColor[bgColor] for _, cell := range entries { if cell.selected { if t.selectedStyle != tcell.StyleDefault { defer colorBackground(cell.x, cell.y, cell.w, cell.h, selBg, selFg, selAttr, false) } else { defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.color, 0, true) } } else { colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, tcell.ColorDefault, 0, false) } } } // Remember column infos. t.visibleColumnIndices, t.visibleColumnWidths = columns, widths } // InputHandler returns the handler for this primitive. func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { t.Lock() defer t.Unlock() key := event.Key() if (!t.rowsSelectable && !t.columnsSelectable && key == tcell.KeyEnter) || key == tcell.KeyEscape || key == tcell.KeyTab || key == tcell.KeyBacktab { if t.done != nil { t.Unlock() t.done(key) t.Lock() } return } // Movement functions. previouslySelectedRow, previouslySelectedColumn := t.selectedRow, t.selectedColumn var ( validSelection = func(row, column int) bool { if row < t.fixedRows || row >= len(t.cells) || column < t.fixedColumns || column > t.lastColumn { return false } cell := t.cells[row][column] return cell == nil || !cell.NotSelectable } home = func() { if t.rowsSelectable { t.selectedRow = 0 t.selectedColumn = 0 } else { t.trackEnd = false t.rowOffset = 0 t.columnOffset = 0 } } end = func() { if t.rowsSelectable { t.selectedRow = len(t.cells) - 1 t.selectedColumn = t.lastColumn } else { t.trackEnd = true t.columnOffset = 0 } } down = func() { if t.rowsSelectable { if validSelection(t.selectedRow+1, t.selectedColumn) { t.selectedRow++ } } else { t.rowOffset++ } } up = func() { if t.rowsSelectable { if validSelection(t.selectedRow-1, t.selectedColumn) { t.selectedRow-- } } else { t.trackEnd = false t.rowOffset-- } } left = func() { if t.columnsSelectable { if validSelection(t.selectedRow, t.selectedColumn-1) { t.selectedColumn-- } } else { t.columnOffset-- } } right = func() { if t.columnsSelectable { if validSelection(t.selectedRow, t.selectedColumn+1) { t.selectedColumn++ } } else { t.columnOffset++ } } pageDown = func() { offsetAmount := t.visibleRows - t.fixedRows if offsetAmount < 0 { offsetAmount = 0 } if t.rowsSelectable { t.selectedRow += offsetAmount if t.selectedRow >= len(t.cells) { t.selectedRow = len(t.cells) - 1 } } else { t.rowOffset += offsetAmount } } pageUp = func() { offsetAmount := t.visibleRows - t.fixedRows if offsetAmount < 0 { offsetAmount = 0 } if t.rowsSelectable { t.selectedRow -= offsetAmount if t.selectedRow < 0 { t.selectedRow = 0 } } else { t.trackEnd = false t.rowOffset -= offsetAmount } } ) if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) { home() } else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) { end() } else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MovePreviousField) { up() } else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) { down() } else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) { left() } else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) { right() } else if HitShortcut(event, Keys.MovePreviousPage) { pageUp() } else if HitShortcut(event, Keys.MoveNextPage) { pageDown() } else if HitShortcut(event, Keys.Select) { if (t.rowsSelectable || t.columnsSelectable) && t.selected != nil { t.Unlock() t.selected(t.selectedRow, t.selectedColumn) t.Lock() } } // If the selection has changed, notify the handler. if t.selectionChanged != nil && ((t.rowsSelectable && previouslySelectedRow != t.selectedRow) || (t.columnsSelectable && previouslySelectedColumn != t.selectedColumn)) { t.Unlock() t.selectionChanged(t.selectedRow, t.selectedColumn) t.Lock() } }) } // MouseHandler returns the mouse handler for this primitive. func (t *Table) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { x, y := event.Position() if !t.InRect(x, y) { return false, nil } switch action { case MouseLeftClick: _, tableY, _, _ := t.GetInnerRect() mul := 1 maxY := tableY if t.cellBorders { mul = 2 maxY = tableY + 1 } if t.sortClicked && t.fixedRows > 0 && (y >= tableY && y < maxY+(t.fixedRows*mul)) { _, column := t.cellAt(x, y) if t.sortClickedColumn != column { t.sortClickedColumn = column t.sortClickedDescending = false } else { t.sortClickedDescending = !t.sortClickedDescending } t.Sort(column, t.sortClickedDescending) if t.columnsSelectable { t.selectedColumn = column } } else if t.rowsSelectable || t.columnsSelectable { t.Select(t.cellAt(x, y)) } consumed = true setFocus(t) case MouseScrollUp: t.trackEnd = false t.rowOffset-- consumed = true case MouseScrollDown: t.rowOffset++ consumed = true } return }) } // SetBorders sets whether or not each cell in the table is surrounded by a border. // // Deprecated: This function is provided for backwards compatibility. // Developers should use SetCellBorder instead. func (t *Table) SetBorders(show bool) { t.SetCellBorders(show) } // SetBordersColor sets the color of the cell borders. // // Deprecated: This function is provided for backwards compatibility. // Developers should use SetCellBorderColor instead. func (t *Table) SetBordersColor(color tcell.Color) { t.SetCellBorderColor(color) }
package cview import ( "bytes" "regexp" "sync" "unicode" "unicode/utf8" "github.com/gdamore/tcell/v2" "github.com/lucasb-eyer/go-colorful" "github.com/mattn/go-runewidth" "github.com/rivo/uniseg" ) var ( openColorRegex = regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`) openRegionRegex = regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`) newLineRegex = regexp.MustCompile(`\r?\n`) // TabSize is the number of spaces with which a tab character will be replaced. TabSize = 4 ) // textViewIndex contains information about each line displayed in the text // view. type textViewIndex struct { Line int // The index into the "buffer" variable. Pos int // The index into the "buffer" line ([]byte position). NextPos int // The (byte) index of the next character in this buffer line. Width int // The screen width of this line. ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset). BackgroundColor string // The starting background color ("" = don't change, "-" = reset). Attributes string // The starting attributes ("" = don't change, "-" = reset). Region []byte // The starting region ID. } // textViewRegion contains information about a region. type textViewRegion struct { // The region ID. ID []byte // The starting and end screen position of the region as determined the last // time Draw() was called. A negative value indicates out-of-rect positions. FromX, FromY, ToX, ToY int } // TextView is a box which displays text. It implements the io.Writer interface // so you can stream text to it. This does not trigger a redraw automatically // but if a handler is installed via SetChangedFunc(), you can cause it to be // redrawn. (See SetChangedFunc() for more details.) // // Navigation // // If the text view is scrollable (the default), text is kept in a buffer which // may be larger than the screen and can be navigated similarly to Vim: // // - h, left arrow: Move left. // - l, right arrow: Move right. // - j, down arrow: Move down. // - k, up arrow: Move up. // - g, home: Move to the top. // - G, end: Move to the bottom. // - Ctrl-F, page down: Move down by one page. // - Ctrl-B, page up: Move up by one page. // // If the text is not scrollable, any text above the top visible line is // discarded. // // Use SetInputCapture() to override or modify keyboard input. // // Colors // // If dynamic colors are enabled via SetDynamicColors(), text color can be // changed dynamically by embedding color strings in square brackets. This works // the same way as anywhere else. Please see the package documentation for more // information. // // Regions and Highlights // // If regions are enabled via SetRegions(), you can define text regions within // the text and assign region IDs to them. Text regions start with region tags. // Region tags are square brackets that contain a region ID in double quotes, // for example: // // We define a ["rg"]region[""] here. // // A text region ends with the next region tag. Tags with no region ID ([""]) // don't start new regions. They can therefore be used to mark the end of a // region. Region IDs must satisfy the following regular expression: // // [a-zA-Z0-9_,;: \-\.]+ // // Regions can be highlighted by calling the Highlight() function with one or // more region IDs. This can be used to display search results, for example. // // The ScrollToHighlight() function can be used to jump to the currently // highlighted region once when the text view is drawn the next time. type TextView struct { *Box // The text buffer. buffer [][]byte // The last bytes that have been received but are not part of the buffer yet. recentBytes []byte // The last width and height of the text view. lastWidth, lastHeight int // The processed line index. This is nil if the buffer has changed and needs // to be re-indexed. index []*textViewIndex // The width of the text view buffer index. indexWidth int // If set to true, the buffer will be reindexed each time it is modified. reindex bool // The text alignment, one of AlignLeft, AlignCenter, or AlignRight. align int // Information about visible regions as of the last call to Draw(). regionInfos []*textViewRegion // Indices into the "index" slice which correspond to the first line of the // first highlight and the last line of the last highlight. This is calculated // during re-indexing. Set to -1 if there is no current highlight. fromHighlight, toHighlight int // The screen space column of the highlight in its first line. Set to -1 if // there is no current highlight. posHighlight int // A set of region IDs that are currently highlighted. highlights map[string]struct{} // The screen width of the longest line in the index (not the buffer). longestLine int // The index of the first line shown in the text view. lineOffset int // The maximum number of newlines the text view will hold (0 = unlimited). maxLines int // If set to true, the text view will always remain at the end of the content. trackEnd bool // The number of characters to be skipped on each line (not in wrap mode). columnOffset int // The height of the content the last time the text view was drawn. pageSize int // If set to true, the text view will keep a buffer of text which can be // navigated when the text is longer than what fits into the box. scrollable bool // Visibility of the scroll bar. scrollBarVisibility ScrollBarVisibility // The scroll bar color. scrollBarColor tcell.Color // If set to true, lines that are longer than the available width are wrapped // onto the next line. If set to false, any characters beyond the available // width are discarded. wrap bool // The maximum line width when wrapping (0 = use TextView width). wrapWidth int // If set to true and if wrap is also true, lines are split at spaces or // after punctuation characters. wordWrap bool // The (starting) color of the text. textColor tcell.Color // If set to true, the text color can be changed dynamically by piping color // strings in square brackets to the text view. dynamicColors bool // If set to true, region tags can be used to define regions. regions bool // A temporary flag which, when true, will automatically bring the current // highlight(s) into the visible screen. scrollToHighlights bool // If true, setting new highlights will be a XOR instead of an overwrite // operation. toggleHighlights bool // An optional function which is called when the content of the text view has // changed. changed func() // An optional function which is called when the user presses one of the // following keys: Escape, Enter, Tab, Backtab. done func(tcell.Key) // An optional function which is called when one or more regions were // highlighted. highlighted func(added, removed, remaining []string) sync.RWMutex } // NewTextView returns a new text view. func NewTextView() *TextView { return &TextView{ Box: NewBox(), highlights: make(map[string]struct{}), lineOffset: -1, reindex: true, scrollable: true, scrollBarVisibility: ScrollBarAuto, scrollBarColor: Styles.ScrollBarColor, align: AlignLeft, wrap: true, textColor: Styles.PrimaryTextColor, } } // SetScrollable sets the flag that decides whether or not the text view is // scrollable. If true, text is kept in a buffer and can be navigated. If false, // the last line will always be visible. func (t *TextView) SetScrollable(scrollable bool) { t.Lock() defer t.Unlock() t.scrollable = scrollable if !scrollable { t.trackEnd = true } } // SetScrollBarVisibility specifies the display of the scroll bar. func (t *TextView) SetScrollBarVisibility(visibility ScrollBarVisibility) { t.Lock() defer t.Unlock() t.scrollBarVisibility = visibility } // SetScrollBarColor sets the color of the scroll bar. func (t *TextView) SetScrollBarColor(color tcell.Color) { t.Lock() defer t.Unlock() t.scrollBarColor = color } // SetWrap sets the flag that, if true, leads to lines that are longer than the // available width being wrapped onto the next line. If false, any characters // beyond the available width are not displayed. func (t *TextView) SetWrap(wrap bool) { t.Lock() defer t.Unlock() if t.wrap != wrap { t.index = nil } t.wrap = wrap } // SetWordWrap sets the flag that, if true and if the "wrap" flag is also true // (see SetWrap()), wraps the line at spaces or after punctuation marks. Note // that trailing spaces will not be printed. // // This flag is ignored if the "wrap" flag is false. func (t *TextView) SetWordWrap(wrapOnWords bool) { t.Lock() defer t.Unlock() if t.wordWrap != wrapOnWords { t.index = nil } t.wordWrap = wrapOnWords } // SetTextAlign sets the text alignment within the text view. This must be // either AlignLeft, AlignCenter, or AlignRight. func (t *TextView) SetTextAlign(align int) { t.Lock() defer t.Unlock() if t.align != align { t.index = nil } t.align = align } // SetTextColor sets the initial color of the text (which can be changed // dynamically by sending color strings in square brackets to the text view if // dynamic colors are enabled). func (t *TextView) SetTextColor(color tcell.Color) { t.Lock() defer t.Unlock() t.textColor = color } // SetBytes sets the text of this text view to the provided byte slice. // Previously contained text will be removed. func (t *TextView) SetBytes(text []byte) { t.Lock() defer t.Unlock() t.clear() t.write(text) } // SetText sets the text of this text view to the provided string. Previously // contained text will be removed. func (t *TextView) SetText(text string) { t.SetBytes([]byte(text)) } // GetBytes returns the current text of this text view. If "stripTags" is set // to true, any region/color tags are stripped from the text. func (t *TextView) GetBytes(stripTags bool) []byte { t.RLock() defer t.RUnlock() if !stripTags { if len(t.recentBytes) > 0 { return bytes.Join(append(t.buffer, t.recentBytes), []byte("\n")) } return bytes.Join(t.buffer, []byte("\n")) } buffer := bytes.Join(t.buffer, []byte("\n")) return StripTags(buffer, t.dynamicColors, t.regions) } // GetText returns the current text of this text view. If "stripTags" is set // to true, any region/color tags are stripped from the text. func (t *TextView) GetText(stripTags bool) string { return string(t.GetBytes(stripTags)) } // SetDynamicColors sets the flag that allows the text color to be changed // dynamically. See class description for details. func (t *TextView) SetDynamicColors(dynamic bool) { t.Lock() defer t.Unlock() if t.dynamicColors != dynamic { t.index = nil } t.dynamicColors = dynamic } // SetRegions sets the flag that allows to define regions in the text. See class // description for details. func (t *TextView) SetRegions(regions bool) { t.Lock() defer t.Unlock() if t.regions != regions { t.index = nil } t.regions = regions } // SetChangedFunc sets a handler function which is called when the text of the // text view has changed. This is useful when text is written to this io.Writer // in a separate goroutine. Doing so does not automatically cause the screen to // be refreshed so you may want to use the "changed" handler to redraw the // screen. // // Note that to avoid race conditions or deadlocks, there are a few rules you // should follow: // // - You can call Application.Draw() from this handler. // - You can call TextView.HasFocus() from this handler. // - During the execution of this handler, access to any other variables from // this primitive or any other primitive should be queued using // Application.QueueUpdate(). // // See package description for details on dealing with concurrency. func (t *TextView) SetChangedFunc(handler func()) { t.Lock() defer t.Unlock() t.changed = handler } // SetDoneFunc sets a handler which is called when the user presses on the // following keys: Escape, Enter, Tab, Backtab. The key is passed to the // handler. func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) { t.Lock() defer t.Unlock() t.done = handler } // SetHighlightedFunc sets a handler which is called when the list of currently // highlighted regions change. It receives a list of region IDs which were newly // highlighted, those that are not highlighted anymore, and those that remain // highlighted. // // Note that because regions are only determined during drawing, this function // can only fire for regions that have existed during the last call to Draw(). func (t *TextView) SetHighlightedFunc(handler func(added, removed, remaining []string)) { t.highlighted = handler } func (t *TextView) clipBuffer() { if t.maxLines <= 0 { return } lenbuf := len(t.buffer) if lenbuf > t.maxLines { t.buffer = t.buffer[lenbuf-t.maxLines:] } } // SetMaxLines sets the maximum number of newlines the text view will hold // before discarding older data from the buffer. func (t *TextView) SetMaxLines(maxLines int) { t.maxLines = maxLines t.clipBuffer() } // ScrollTo scrolls to the specified row and column (both starting with 0). func (t *TextView) ScrollTo(row, column int) { t.Lock() defer t.Unlock() if !t.scrollable { return } t.lineOffset = row t.columnOffset = column t.trackEnd = false } // ScrollToBeginning scrolls to the top left corner of the text if the text view // is scrollable. func (t *TextView) ScrollToBeginning() { t.Lock() defer t.Unlock() if !t.scrollable { return } t.trackEnd = false t.lineOffset = 0 t.columnOffset = 0 } // ScrollToEnd scrolls to the bottom left corner of the text if the text view // is scrollable. Adding new rows to the end of the text view will cause it to // scroll with the new data. func (t *TextView) ScrollToEnd() { t.Lock() defer t.Unlock() if !t.scrollable { return } t.trackEnd = true t.columnOffset = 0 } // GetScrollOffset returns the number of rows and columns that are skipped at // the top left corner when the text view has been scrolled. func (t *TextView) GetScrollOffset() (row, column int) { t.RLock() defer t.RUnlock() return t.lineOffset, t.columnOffset } // Clear removes all text from the buffer. func (t *TextView) Clear() { t.Lock() defer t.Unlock() t.clear() } func (t *TextView) clear() { t.buffer = nil t.recentBytes = nil if t.reindex { t.index = nil } } // Highlight specifies which regions should be highlighted. If highlight // toggling is set to true (see SetToggleHighlights()), the highlight of the // provided regions is toggled (highlighted regions are un-highlighted and vice // versa). If toggling is set to false, the provided regions are highlighted and // all other regions will not be highlighted (you may also provide nil to turn // off all highlights). // // For more information on regions, see class description. Empty region strings // are ignored. // // Text in highlighted regions will be drawn inverted, i.e. with their // background and foreground colors swapped. func (t *TextView) Highlight(regionIDs ...string) { t.Lock() // Toggle highlights. if t.toggleHighlights { var newIDs []string HighlightLoop: for regionID := range t.highlights { for _, id := range regionIDs { if regionID == id { continue HighlightLoop } } newIDs = append(newIDs, regionID) } for _, regionID := range regionIDs { if _, ok := t.highlights[regionID]; !ok { newIDs = append(newIDs, regionID) } } regionIDs = newIDs } // Now we have a list of region IDs that end up being highlighted. // Determine added and removed regions. var added, removed, remaining []string if t.highlighted != nil { for _, regionID := range regionIDs { if _, ok := t.highlights[regionID]; ok { remaining = append(remaining, regionID) delete(t.highlights, regionID) } else { added = append(added, regionID) } } for regionID := range t.highlights { removed = append(removed, regionID) } } // Make new selection. t.highlights = make(map[string]struct{}) for _, id := range regionIDs { if id == "" { continue } t.highlights[id] = struct{}{} } t.index = nil // Notify. if t.highlighted != nil && (len(added) > 0 || len(removed) > 0) { t.Unlock() t.highlighted(added, removed, remaining) } else { t.Unlock() } } // GetHighlights returns the IDs of all currently highlighted regions. func (t *TextView) GetHighlights() (regionIDs []string) { t.RLock() defer t.RUnlock() for id := range t.highlights { regionIDs = append(regionIDs, id) } return } // SetToggleHighlights sets a flag to determine how regions are highlighted. // When set to true, the Highlight() function (or a mouse click) will toggle the // provided/selected regions. When set to false, Highlight() (or a mouse click) // will simply highlight the provided regions. func (t *TextView) SetToggleHighlights(toggle bool) { t.toggleHighlights = toggle } // ScrollToHighlight will cause the visible area to be scrolled so that the // highlighted regions appear in the visible area of the text view. This // repositioning happens the next time the text view is drawn. It happens only // once so you will need to call this function repeatedly to always keep // highlighted regions in view. // // Nothing happens if there are no highlighted regions or if the text view is // not scrollable. func (t *TextView) ScrollToHighlight() { t.Lock() defer t.Unlock() if len(t.highlights) == 0 || !t.scrollable || !t.regions { return } t.index = nil t.scrollToHighlights = true t.trackEnd = false } // GetRegionText returns the text of the region with the given ID. If dynamic // colors are enabled, color tags are stripped from the text. Newlines are // always returned as '\n' runes. // // If the region does not exist or if regions are turned off, an empty string // is returned. func (t *TextView) GetRegionText(regionID string) string { t.RLock() defer t.RUnlock() if !t.regions || len(regionID) == 0 { return "" } var ( buffer bytes.Buffer currentRegionID string ) for _, str := range t.buffer { // Find all color tags in this line. var colorTagIndices [][]int if t.dynamicColors { colorTagIndices = colorPattern.FindAllIndex(str, -1) } // Find all regions in this line. var ( regionIndices [][]int regions [][][]byte ) if t.regions { regionIndices = regionPattern.FindAllIndex(str, -1) regions = regionPattern.FindAllSubmatch(str, -1) } // Analyze this line. var currentTag, currentRegion int for pos, ch := range str { // Skip any color tags. if currentTag < len(colorTagIndices) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] { if pos == colorTagIndices[currentTag][1]-1 { currentTag++ } if colorTagIndices[currentTag][1]-colorTagIndices[currentTag][0] > 2 { continue } } // Skip any regions. if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] { if pos == regionIndices[currentRegion][1]-1 { if currentRegionID == regionID { // This is the end of the requested region. We're done. return buffer.String() } currentRegionID = string(regions[currentRegion][1]) currentRegion++ } continue } // Add this rune. if currentRegionID == regionID { buffer.WriteByte(ch) } } // Add newline. if currentRegionID == regionID { buffer.WriteRune('\n') } } return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`) } // Focus is called when this primitive receives focus. func (t *TextView) Focus(delegate func(p Primitive)) { t.Lock() defer t.Unlock() // Implemented here with locking because this is used by layout primitives. t.hasFocus = true } // HasFocus returns whether or not this primitive has focus. func (t *TextView) HasFocus() bool { t.RLock() defer t.RUnlock() // Implemented here with locking because this may be used in the "changed" // callback. return t.hasFocus } // Write lets us implement the io.Writer interface. Tab characters will be // replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted // as a new line. func (t *TextView) Write(p []byte) (n int, err error) { t.Lock() changed := t.changed if changed != nil { // Notify at the end. defer changed() } defer t.Unlock() return t.write(p) } func (t *TextView) write(p []byte) (n int, err error) { // Copy data over. newBytes := append(t.recentBytes, p...) t.recentBytes = nil // If we have a trailing invalid UTF-8 byte, we'll wait. if r, _ := utf8.DecodeLastRune(p); r == utf8.RuneError { t.recentBytes = newBytes return len(p), nil } // If we have a trailing open dynamic color, exclude it. if t.dynamicColors { location := openColorRegex.FindIndex(newBytes) if location != nil { t.recentBytes = newBytes[location[0]:] newBytes = newBytes[:location[0]] } } // If we have a trailing open region, exclude it. if t.regions { location := openRegionRegex.FindIndex(newBytes) if location != nil { t.recentBytes = newBytes[location[0]:] newBytes = newBytes[:location[0]] } } // Transform the new bytes into strings. newBytes = bytes.Replace(newBytes, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1) for index, line := range bytes.Split(newBytes, []byte("\n")) { if index == 0 { if len(t.buffer) == 0 { t.buffer = [][]byte{line} } else { t.buffer[len(t.buffer)-1] = append(t.buffer[len(t.buffer)-1], line...) } } else { t.buffer = append(t.buffer, line) } } t.clipBuffer() // Reset the index. if t.reindex { t.index = nil } return len(p), nil } // SetWrapWidth set the maximum width of lines when wrapping is enabled. // When set to 0 the width of the TextView is used. func (t *TextView) SetWrapWidth(width int) { t.Lock() defer t.Unlock() t.wrapWidth = width } // SetReindexBuffer set a flag controlling whether the buffer is reindexed when // it is modified. This improves the performance of TextViews whose contents // always have line-breaks in the same location. This must be called after the // buffer has been indexed. func (t *TextView) SetReindexBuffer(reindex bool) { t.Lock() defer t.Unlock() t.reindex = reindex if reindex { t.index = nil } } // reindexBuffer re-indexes the buffer such that we can use it to easily draw // the buffer onto the screen. Each line in the index will contain a pointer // into the buffer from which on we will print text. It will also contain the // color with which the line starts. func (t *TextView) reindexBuffer(width int) { if t.index != nil && (!t.wrap || width == t.indexWidth) { return // Nothing has changed. We can still use the current index. } t.index = nil t.indexWidth = width t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1 // If there's no space, there's no index. if width < 1 { return } if t.wrapWidth > 0 && t.wrapWidth < width { width = t.wrapWidth } // Initial states. var regionID []byte var ( highlighted bool foregroundColor, backgroundColor, attributes string ) // Go through each line in the buffer. for bufferIndex, buf := range t.buffer { colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedStr, _ := decomposeText(buf, t.dynamicColors, t.regions) // Split the line if required. var splitLines []string str := string(strippedStr) if t.wrap && len(str) > 0 { for len(str) > 0 { extract := runewidth.Truncate(str, width, "") if len(extract) == 0 { // We'll extract at least one grapheme cluster. gr := uniseg.NewGraphemes(str) gr.Next() _, to := gr.Positions() extract = str[:to] } if t.wordWrap && len(extract) < len(str) { // Add any spaces from the next line. if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 { extract = str[:len(extract)+spaces[1]] } // Can we split before the mandatory end? matches := boundaryPattern.FindAllStringIndex(extract, -1) if len(matches) > 0 { // Yes. Let's split there. extract = extract[:matches[len(matches)-1][1]] } } splitLines = append(splitLines, extract) str = str[len(extract):] } } else { // No need to split the line. splitLines = []string{str} } // Create index from split lines. var originalPos, colorPos, regionPos, escapePos int for _, splitLine := range splitLines { line := &textViewIndex{ Line: bufferIndex, Pos: originalPos, ForegroundColor: foregroundColor, BackgroundColor: backgroundColor, Attributes: attributes, Region: regionID, } // Shift original position with tags. lineLength := len(splitLine) remainingLength := lineLength tagEnd := originalPos totalTagLength := 0 for { // Which tag comes next? nextTag := make([][3]int, 0, 3) if colorPos < len(colorTagIndices) { nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0}) // 0 = color tag. } if regionPos < len(regionIndices) { nextTag = append(nextTag, [3]int{regionIndices[regionPos][0], regionIndices[regionPos][1], 1}) // 1 = region tag. } if escapePos < len(escapeIndices) { nextTag = append(nextTag, [3]int{escapeIndices[escapePos][0], escapeIndices[escapePos][1], 2}) // 2 = escape tag. } minPos := -1 tagIndex := -1 for index, pair := range nextTag { if minPos < 0 || pair[0] < minPos { minPos = pair[0] tagIndex = index } } // Is the next tag in range? if tagIndex < 0 || minPos > tagEnd+remainingLength { break // No. We're done with this line. } // Advance. strippedTagStart := nextTag[tagIndex][0] - originalPos - totalTagLength tagEnd = nextTag[tagIndex][1] tagLength := tagEnd - nextTag[tagIndex][0] if nextTag[tagIndex][2] == 2 { tagLength = 1 } totalTagLength += tagLength remainingLength = lineLength - (tagEnd - originalPos - totalTagLength) // Process the tag. switch nextTag[tagIndex][2] { case 0: // Process color tags. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) colorPos++ case 1: // Process region tags. regionID = regions[regionPos][1] _, highlighted = t.highlights[string(regionID)] // Update highlight range. if highlighted { line := len(t.index) if t.fromHighlight < 0 { t.fromHighlight, t.toHighlight = line, line t.posHighlight = runewidth.StringWidth(splitLine[:strippedTagStart]) } else if line > t.toHighlight { t.toHighlight = line } } regionPos++ case 2: // Process escape tags. escapePos++ } } // Advance to next line. originalPos += lineLength + totalTagLength // Append this line. line.NextPos = originalPos line.Width = runewidth.StringWidth(splitLine) t.index = append(t.index, line) } // Word-wrapped lines may have trailing whitespace. Remove it. if t.wrap && t.wordWrap { for _, line := range t.index { str := t.buffer[line.Line][line.Pos:line.NextPos] trimmed := bytes.TrimRightFunc(str, unicode.IsSpace) if len(trimmed) != len(str) { oldNextPos := line.NextPos line.NextPos -= len(str) - len(trimmed) line.Width -= runewidth.StringWidth(string(t.buffer[line.Line][line.NextPos:oldNextPos])) } } } } // Calculate longest line. t.longestLine = 0 for _, line := range t.index { if line.Width > t.longestLine { t.longestLine = line.Width } } } // Draw draws this primitive onto the screen. func (t *TextView) Draw(screen tcell.Screen) { if !t.GetVisible() { return } t.Box.Draw(screen) t.Lock() defer t.Unlock() // Get the available size. x, y, width, height := t.GetInnerRect() if height == 0 { return } t.pageSize = height if t.index == nil || width != t.lastWidth || height != t.lastHeight { t.reindexBuffer(width) } t.lastWidth, t.lastHeight = width, height showVerticalScrollBar := t.scrollBarVisibility == ScrollBarAlways || (t.scrollBarVisibility == ScrollBarAuto && len(t.index) > height) if showVerticalScrollBar { width-- // Subtract space for scroll bar. } t.reindexBuffer(width) if t.regions { t.regionInfos = nil } // If we don't have an index, there's nothing to draw. if t.index == nil { return } // Move to highlighted regions. if t.regions && t.scrollToHighlights && t.fromHighlight >= 0 { // Do we fit the entire height? if t.toHighlight-t.fromHighlight+1 < height { // Yes, let's center the highlights. t.lineOffset = (t.fromHighlight + t.toHighlight - height) / 2 } else { // No, let's move to the start of the highlights. t.lineOffset = t.fromHighlight } // If the highlight is too far to the right, move it to the middle. if t.posHighlight-t.columnOffset > 3*width/4 { t.columnOffset = t.posHighlight - width/2 } // If the highlight is offscreen on the left, move it onscreen. if t.posHighlight-t.columnOffset < 0 { t.columnOffset = t.posHighlight - width/4 } } t.scrollToHighlights = false // Adjust line offset. if t.lineOffset+height > len(t.index) { t.trackEnd = true } if t.trackEnd { t.lineOffset = len(t.index) - height } if t.lineOffset < 0 { t.lineOffset = 0 } // Adjust column offset. if t.align == AlignLeft { if t.columnOffset+width > t.longestLine { t.columnOffset = t.longestLine - width } if t.columnOffset < 0 { t.columnOffset = 0 } } else if t.align == AlignRight { if t.columnOffset-width < -t.longestLine { t.columnOffset = width - t.longestLine } if t.columnOffset > 0 { t.columnOffset = 0 } } else { // AlignCenter. half := (t.longestLine - width) / 2 if half > 0 { if t.columnOffset > half { t.columnOffset = half } if t.columnOffset < -half { t.columnOffset = -half } } else { t.columnOffset = 0 } } // Draw the buffer. defaultStyle := tcell.StyleDefault.Foreground(t.textColor) for line := t.lineOffset; line < len(t.index); line++ { // Are we done? if line-t.lineOffset >= height { break } // Get the text for this line. index := t.index[line] text := t.buffer[index.Line][index.Pos:index.NextPos] foregroundColor := index.ForegroundColor backgroundColor := index.BackgroundColor attributes := index.Attributes regionID := index.Region if t.regions && len(regionID) > 0 && (len(t.regionInfos) == 0 || !bytes.Equal(t.regionInfos[len(t.regionInfos)-1].ID, regionID)) { t.regionInfos = append(t.regionInfos, &textViewRegion{ ID: regionID, FromX: x, FromY: y + line - t.lineOffset, ToX: -1, ToY: -1, }) } // Process tags. colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeText(text, t.dynamicColors, t.regions) // Calculate the position of the line. var skip, posX int if t.align == AlignLeft { posX = -t.columnOffset } else if t.align == AlignRight { posX = width - index.Width - t.columnOffset } else { // AlignCenter. posX = (width-index.Width)/2 - t.columnOffset } if posX < 0 { skip = -posX posX = 0 } // Print the line. if y+line-t.lineOffset >= 0 { var colorPos, regionPos, escapePos, tagOffset, skipped int iterateString(string(strippedText), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Process tags. for { if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { // Get the color. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] colorPos++ } else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] { // Get the region. if len(regionID) > 0 && len(t.regionInfos) > 0 && bytes.Equal(t.regionInfos[len(t.regionInfos)-1].ID, regionID) { // End last region. t.regionInfos[len(t.regionInfos)-1].ToX = x + posX t.regionInfos[len(t.regionInfos)-1].ToY = y + line - t.lineOffset } regionID = regions[regionPos][1] if len(regionID) > 0 { // Start new region. t.regionInfos = append(t.regionInfos, &textViewRegion{ ID: regionID, FromX: x + posX, FromY: y + line - t.lineOffset, ToX: -1, ToY: -1, }) } tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0] regionPos++ } else { break } } // Skip the second-to-last character of an escape tag. if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 { tagOffset++ escapePos++ } // Mix the existing style with the new style. _, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset) _, background, _ := existingStyle.Decompose() style := overlayStyle(background, defaultStyle, foregroundColor, backgroundColor, attributes) // Do we highlight this character? var highlighted bool if len(regionID) > 0 { if _, ok := t.highlights[string(regionID)]; ok { highlighted = true } } if highlighted { fg, bg, _ := style.Decompose() if bg == tcell.ColorDefault { r, g, b := fg.RGB() c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255} _, _, li := c.Hcl() if li < .5 { bg = tcell.ColorWhite.TrueColor() } else { bg = tcell.ColorBlack.TrueColor() } } style = style.Background(fg).Foreground(bg) } // Skip to the right. if !t.wrap && skipped < skip { skipped += screenWidth return false } // Stop at the right border. if posX+screenWidth > width { return true } // Draw the character. for offset := screenWidth - 1; offset >= 0; offset-- { if offset == 0 { screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style) } else { screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style) } } // Advance. posX += screenWidth return false }) } } // Draw scroll bar. if showVerticalScrollBar { cursor := int(float64(len(t.index)) * (float64(t.lineOffset) / float64(len(t.index)-height))) for printed := 0; printed < height; printed++ { RenderScrollBar(screen, t.scrollBarVisibility, x+width, y+printed, height, len(t.index), cursor, printed, t.hasFocus, t.scrollBarColor) } } // If this view is not scrollable, we'll purge the buffer of lines that have // scrolled out of view. if !t.scrollable && t.lineOffset > 0 { if t.lineOffset >= len(t.index) { t.buffer = nil } else { t.buffer = t.buffer[t.index[t.lineOffset].Line:] } t.index = nil t.lineOffset = 0 } } // InputHandler returns the handler for this primitive. func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { key := event.Key() if HitShortcut(event, Keys.Cancel, Keys.Select, Keys.Select2, Keys.MovePreviousField, Keys.MoveNextField) { if t.done != nil { t.done(key) } return } t.Lock() defer t.Unlock() if !t.scrollable { return } if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) { t.trackEnd = false t.lineOffset = 0 t.columnOffset = 0 } else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) { t.trackEnd = true t.columnOffset = 0 } else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2) { t.trackEnd = false t.lineOffset-- } else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2) { t.lineOffset++ } else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) { t.columnOffset-- } else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) { t.columnOffset++ } else if HitShortcut(event, Keys.MovePreviousPage) { t.trackEnd = false t.lineOffset -= t.pageSize } else if HitShortcut(event, Keys.MoveNextPage) { t.lineOffset += t.pageSize } }) } // MouseHandler returns the mouse handler for this primitive. func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { x, y := event.Position() if !t.InRect(x, y) { return false, nil } switch action { case MouseLeftClick: if t.regions { // Find a region to highlight. for _, region := range t.regionInfos { if y == region.FromY && x < region.FromX || y == region.ToY && x >= region.ToX || region.FromY >= 0 && y < region.FromY || region.ToY >= 0 && y > region.ToY { continue } t.Highlight(string(region.ID)) break } } consumed = true setFocus(t) case MouseScrollUp: if t.scrollable { t.trackEnd = false t.lineOffset-- consumed = true } case MouseScrollDown: if t.scrollable { t.lineOffset++ consumed = true } } return }) }
package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // Tree navigation events. const ( treeNone int = iota treeHome treeEnd treeUp treeDown treePageUp treePageDown ) // TreeNode represents one node in a tree view. type TreeNode struct { // The reference object. reference interface{} // This node's child nodes. children []*TreeNode // The item's text. text string // The text color. color tcell.Color // Whether or not this node can be focused and selected. selectable bool // Whether or not this node's children should be displayed. expanded bool // The additional horizontal indent of this node's text. indent int // An optional function which is called when the user focuses this node. focused func() // An optional function which is called when the user selects this node. selected func() // Temporary member variables. parent *TreeNode // The parent node (nil for the root). level int // The hierarchy level (0 for the root, 1 for its children, and so on). graphicsX int // The x-coordinate of the left-most graphics rune. textX int // The x-coordinate of the first rune of the text. sync.RWMutex } // NewTreeNode returns a new tree node. func NewTreeNode(text string) *TreeNode { return &TreeNode{ text: text, color: Styles.PrimaryTextColor, indent: 2, expanded: true, selectable: true, } } // Walk traverses this node's subtree in depth-first, pre-order (NLR) order and // calls the provided callback function on each traversed node (which includes // this node) with the traversed node and its parent node (nil for this node). // The callback returns whether traversal should continue with the traversed // node's child nodes (true) or not recurse any deeper (false). func (n *TreeNode) Walk(callback func(node, parent *TreeNode) bool) { n.Lock() defer n.Unlock() n.walk(callback) } func (n *TreeNode) walk(callback func(node, parent *TreeNode) bool) { n.parent = nil nodes := []*TreeNode{n} for len(nodes) > 0 { // Pop the top node and process it. node := nodes[len(nodes)-1] nodes = nodes[:len(nodes)-1] if !callback(node, node.parent) { // Don't add any children. continue } // Add children in reverse order. for index := len(node.children) - 1; index >= 0; index-- { node.children[index].parent = node nodes = append(nodes, node.children[index]) } } } // SetReference allows you to store a reference of any type in this node. This // will allow you to establish a mapping between the TreeView hierarchy and your // internal tree structure. func (n *TreeNode) SetReference(reference interface{}) { n.Lock() defer n.Unlock() n.reference = reference } // GetReference returns this node's reference object. func (n *TreeNode) GetReference() interface{} { n.RLock() defer n.RUnlock() return n.reference } // SetChildren sets this node's child nodes. func (n *TreeNode) SetChildren(childNodes []*TreeNode) { n.Lock() defer n.Unlock() n.children = childNodes } // GetText returns this node's text. func (n *TreeNode) GetText() string { n.RLock() defer n.RUnlock() return n.text } // GetChildren returns this node's children. func (n *TreeNode) GetChildren() []*TreeNode { n.RLock() defer n.RUnlock() return n.children } // ClearChildren removes all child nodes from this node. func (n *TreeNode) ClearChildren() { n.Lock() defer n.Unlock() n.children = nil } // AddChild adds a new child node to this node. func (n *TreeNode) AddChild(node *TreeNode) { n.Lock() defer n.Unlock() n.children = append(n.children, node) } // SetSelectable sets a flag indicating whether this node can be focused and // selected by the user. func (n *TreeNode) SetSelectable(selectable bool) { n.Lock() defer n.Unlock() n.selectable = selectable } // SetFocusedFunc sets the function which is called when the user navigates to // this node. // // This function is also called when the user selects this node. func (n *TreeNode) SetFocusedFunc(handler func()) { n.Lock() defer n.Unlock() n.focused = handler } // SetSelectedFunc sets a function which is called when the user selects this // node by hitting Enter when it is focused. func (n *TreeNode) SetSelectedFunc(handler func()) { n.Lock() defer n.Unlock() n.selected = handler } // SetExpanded sets whether or not this node's child nodes should be displayed. func (n *TreeNode) SetExpanded(expanded bool) { n.Lock() defer n.Unlock() n.expanded = expanded } // Expand makes the child nodes of this node appear. func (n *TreeNode) Expand() { n.Lock() defer n.Unlock() n.expanded = true } // Collapse makes the child nodes of this node disappear. func (n *TreeNode) Collapse() { n.Lock() defer n.Unlock() n.expanded = false } // ExpandAll expands this node and all descendent nodes. func (n *TreeNode) ExpandAll() { n.Walk(func(node, parent *TreeNode) bool { node.expanded = true return true }) } // CollapseAll collapses this node and all descendent nodes. func (n *TreeNode) CollapseAll() { n.Walk(func(node, parent *TreeNode) bool { n.expanded = false return true }) } // IsExpanded returns whether the child nodes of this node are visible. func (n *TreeNode) IsExpanded() bool { n.RLock() defer n.RUnlock() return n.expanded } // SetText sets the node's text which is displayed. func (n *TreeNode) SetText(text string) { n.Lock() defer n.Unlock() n.text = text } // GetColor returns the node's color. func (n *TreeNode) GetColor() tcell.Color { n.RLock() defer n.RUnlock() return n.color } // SetColor sets the node's text color. func (n *TreeNode) SetColor(color tcell.Color) { n.Lock() defer n.Unlock() n.color = color } // SetIndent sets an additional indentation for this node's text. A value of 0 // keeps the text as far left as possible with a minimum of line graphics. Any // value greater than that moves the text to the right. func (n *TreeNode) SetIndent(indent int) { n.Lock() defer n.Unlock() n.indent = indent } // TreeView displays tree structures. A tree consists of nodes (TreeNode // objects) where each node has zero or more child nodes and exactly one parent // node (except for the root node which has no parent node). // // The SetRoot() function is used to specify the root of the tree. Other nodes // are added locally to the root node or any of its descendents. See the // TreeNode documentation for details on node attributes. (You can use // SetReference() to store a reference to nodes of your own tree structure.) // // Nodes can be focused by calling SetCurrentNode(). The user can navigate the // selection or the tree by using the following keys: // // - j, down arrow, right arrow: Move (the selection) down by one node. // - k, up arrow, left arrow: Move (the selection) up by one node. // - g, home: Move (the selection) to the top. // - G, end: Move (the selection) to the bottom. // - Ctrl-F, page down: Move (the selection) down by one page. // - Ctrl-B, page up: Move (the selection) up by one page. // // Selected nodes can trigger the "selected" callback when the user hits Enter. // // The root node corresponds to level 0, its children correspond to level 1, // their children to level 2, and so on. Per default, the first level that is // displayed is 0, i.e. the root node. You can call SetTopLevel() to hide // levels. // // If graphics are turned on (see SetGraphics()), lines indicate the tree's // hierarchy. Alternative (or additionally), you can set different prefixes // using SetPrefixes() for different levels, for example to display hierarchical // bullet point lists. type TreeView struct { *Box // The root node. root *TreeNode // The currently focused node or nil if no node is focused. currentNode *TreeNode // The movement to be performed during the call to Draw(), one of the // constants defined above. movement int // The top hierarchical level shown. (0 corresponds to the root level.) topLevel int // Strings drawn before the nodes, based on their level. prefixes [][]byte // Vertical scroll offset. offsetY int // If set to true, all node texts will be aligned horizontally. align bool // If set to true, the tree structure is drawn using lines. graphics bool // The text color for selected items. selectedTextColor *tcell.Color // The background color for selected items. selectedBackgroundColor *tcell.Color // The color of the lines. graphicsColor tcell.Color // Visibility of the scroll bar. scrollBarVisibility ScrollBarVisibility // The scroll bar color. scrollBarColor tcell.Color // An optional function called when the focused tree item changes. changed func(node *TreeNode) // An optional function called when a tree item is selected. selected func(node *TreeNode) // An optional function called when the user moves away from this primitive. done func(key tcell.Key) // The visible nodes, top-down, as set by process(). nodes []*TreeNode sync.RWMutex } // NewTreeView returns a new tree view. func NewTreeView() *TreeView { return &TreeView{ Box: NewBox(), scrollBarVisibility: ScrollBarAuto, graphics: true, graphicsColor: Styles.GraphicsColor, scrollBarColor: Styles.ScrollBarColor, } } // SetRoot sets the root node of the tree. func (t *TreeView) SetRoot(root *TreeNode) { t.Lock() defer t.Unlock() t.root = root } // GetRoot returns the root node of the tree. If no such node was previously // set, nil is returned. func (t *TreeView) GetRoot() *TreeNode { t.RLock() defer t.RUnlock() return t.root } // SetCurrentNode focuses a node or, when provided with nil, clears focus. // Selected nodes must be visible and selectable, or else the selection will be // changed to the top-most selectable and visible node. // // This function does NOT trigger the "changed" callback. func (t *TreeView) SetCurrentNode(node *TreeNode) { t.Lock() defer t.Unlock() t.currentNode = node if t.currentNode.focused != nil { t.Unlock() t.currentNode.focused() t.Lock() } } // GetCurrentNode returns the currently selected node or nil of no node is // currently selected. func (t *TreeView) GetCurrentNode() *TreeNode { t.RLock() defer t.RUnlock() return t.currentNode } // SetTopLevel sets the first tree level that is visible with 0 referring to the // root, 1 to the root's child nodes, and so on. Nodes above the top level are // not displayed. func (t *TreeView) SetTopLevel(topLevel int) { t.Lock() defer t.Unlock() t.topLevel = topLevel } // SetPrefixes defines the strings drawn before the nodes' texts. This is a // slice of strings where each element corresponds to a node's hierarchy level, // i.e. 0 for the root, 1 for the root's children, and so on (levels will // cycle). // // For example, to display a hierarchical list with bullet points: // // treeView.SetGraphics(false). // SetPrefixes([]string{"* ", "- ", "x "}) func (t *TreeView) SetPrefixes(prefixes []string) { t.Lock() defer t.Unlock() t.prefixes = make([][]byte, len(prefixes)) for i := range prefixes { t.prefixes[i] = []byte(prefixes[i]) } } // SetAlign controls the horizontal alignment of the node texts. If set to true, // all texts except that of top-level nodes will be placed in the same column. // If set to false, they will indent with the hierarchy. func (t *TreeView) SetAlign(align bool) { t.Lock() defer t.Unlock() t.align = align } // SetGraphics sets a flag which determines whether or not line graphics are // drawn to illustrate the tree's hierarchy. func (t *TreeView) SetGraphics(showGraphics bool) { t.Lock() defer t.Unlock() t.graphics = showGraphics } // SetSelectedTextColor sets the text color of selected items. func (t *TreeView) SetSelectedTextColor(color tcell.Color) { t.Lock() defer t.Unlock() t.selectedTextColor = &color } // SetSelectedBackgroundColor sets the background color of selected items. func (t *TreeView) SetSelectedBackgroundColor(color tcell.Color) { t.Lock() defer t.Unlock() t.selectedBackgroundColor = &color } // SetGraphicsColor sets the colors of the lines used to draw the tree structure. func (t *TreeView) SetGraphicsColor(color tcell.Color) { t.Lock() defer t.Unlock() t.graphicsColor = color } // SetScrollBarVisibility specifies the display of the scroll bar. func (t *TreeView) SetScrollBarVisibility(visibility ScrollBarVisibility) { t.Lock() defer t.Unlock() t.scrollBarVisibility = visibility } // SetScrollBarColor sets the color of the scroll bar. func (t *TreeView) SetScrollBarColor(color tcell.Color) { t.Lock() defer t.Unlock() t.scrollBarColor = color } // SetChangedFunc sets the function which is called when the user navigates to // a new tree node. func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) { t.Lock() defer t.Unlock() t.changed = handler } // SetSelectedFunc sets the function which is called when the user selects a // node by pressing Enter on the current selection. func (t *TreeView) SetSelectedFunc(handler func(node *TreeNode)) { t.Lock() defer t.Unlock() t.selected = handler } // SetDoneFunc sets a handler which is called whenever the user presses the // Escape, Tab, or Backtab key. func (t *TreeView) SetDoneFunc(handler func(key tcell.Key)) { t.Lock() defer t.Unlock() t.done = handler } // GetScrollOffset returns the number of node rows that were skipped at the top // of the tree view. Note that when the user navigates the tree view, this value // is only updated after the tree view has been redrawn. func (t *TreeView) GetScrollOffset() int { t.RLock() defer t.RUnlock() return t.offsetY } // GetRowCount returns the number of "visible" nodes. This includes nodes which // fall outside the tree view's box but notably does not include the children // of collapsed nodes. Note that this value is only up to date after the tree // view has been drawn. func (t *TreeView) GetRowCount() int { t.RLock() defer t.RUnlock() return len(t.nodes) } // Transform modifies the current selection. func (t *TreeView) Transform(tr Transformation) { t.Lock() defer t.Unlock() switch tr { case TransformFirstItem: t.movement = treeHome case TransformLastItem: t.movement = treeEnd case TransformPreviousItem: t.movement = treeUp case TransformNextItem: t.movement = treeDown case TransformPreviousPage: t.movement = treePageUp case TransformNextPage: t.movement = treePageDown } t.process() } // process builds the visible tree, populates the "nodes" slice, and processes // pending selection actions. func (t *TreeView) process() { _, _, _, height := t.GetInnerRect() // Determine visible nodes and their placement. var graphicsOffset, maxTextX int t.nodes = nil selectedIndex := -1 topLevelGraphicsX := -1 if t.graphics { graphicsOffset = 1 } t.root.walk(func(node, parent *TreeNode) bool { // Set node attributes. node.parent = parent if parent == nil { node.level = 0 node.graphicsX = 0 node.textX = 0 } else { node.level = parent.level + 1 node.graphicsX = parent.textX node.textX = node.graphicsX + graphicsOffset + node.indent } if !t.graphics && t.align { // Without graphics, we align nodes on the first column. node.textX = 0 } if node.level == t.topLevel { // No graphics for top level nodes. node.graphicsX = 0 node.textX = 0 } // Add the node to the list. if node.level >= t.topLevel { // This node will be visible. if node.textX > maxTextX { maxTextX = node.textX } if node == t.currentNode && node.selectable { selectedIndex = len(t.nodes) } // Maybe we want to skip this level. if t.topLevel == node.level && (topLevelGraphicsX < 0 || node.graphicsX < topLevelGraphicsX) { topLevelGraphicsX = node.graphicsX } t.nodes = append(t.nodes, node) } // Recurse if desired. return node.expanded }) // Post-process positions. for _, node := range t.nodes { // If text must align, we correct the positions. if t.align && node.level > t.topLevel { node.textX = maxTextX } // If we skipped levels, shift to the left. if topLevelGraphicsX > 0 { node.graphicsX -= topLevelGraphicsX node.textX -= topLevelGraphicsX } } // Process selection. (Also trigger events if necessary.) if selectedIndex >= 0 { // Move the selection. newSelectedIndex := selectedIndex MovementSwitch: switch t.movement { case treeUp: for newSelectedIndex > 0 { newSelectedIndex-- if t.nodes[newSelectedIndex].selectable { break MovementSwitch } } newSelectedIndex = selectedIndex case treeDown: for newSelectedIndex < len(t.nodes)-1 { newSelectedIndex++ if t.nodes[newSelectedIndex].selectable { break MovementSwitch } } newSelectedIndex = selectedIndex case treeHome: for newSelectedIndex = 0; newSelectedIndex < len(t.nodes); newSelectedIndex++ { if t.nodes[newSelectedIndex].selectable { break MovementSwitch } } newSelectedIndex = selectedIndex case treeEnd: for newSelectedIndex = len(t.nodes) - 1; newSelectedIndex >= 0; newSelectedIndex-- { if t.nodes[newSelectedIndex].selectable { break MovementSwitch } } newSelectedIndex = selectedIndex case treePageDown: if newSelectedIndex+height < len(t.nodes) { newSelectedIndex += height } else { newSelectedIndex = len(t.nodes) - 1 } for ; newSelectedIndex < len(t.nodes); newSelectedIndex++ { if t.nodes[newSelectedIndex].selectable { break MovementSwitch } } newSelectedIndex = selectedIndex case treePageUp: if newSelectedIndex >= height { newSelectedIndex -= height } else { newSelectedIndex = 0 } for ; newSelectedIndex >= 0; newSelectedIndex-- { if t.nodes[newSelectedIndex].selectable { break MovementSwitch } } newSelectedIndex = selectedIndex } t.currentNode = t.nodes[newSelectedIndex] if newSelectedIndex != selectedIndex { t.movement = treeNone if t.changed != nil { t.Unlock() t.changed(t.currentNode) t.Lock() } if t.currentNode.focused != nil { t.Unlock() t.currentNode.focused() t.Lock() } } selectedIndex = newSelectedIndex // Move selection into viewport. if selectedIndex-t.offsetY >= height { t.offsetY = selectedIndex - height + 1 } if selectedIndex < t.offsetY { t.offsetY = selectedIndex } } else { // If selection is not visible or selectable, select the first candidate. if t.currentNode != nil { for index, node := range t.nodes { if node.selectable { selectedIndex = index t.currentNode = node break } } } if selectedIndex < 0 { t.currentNode = nil } } } // Draw draws this primitive onto the screen. func (t *TreeView) Draw(screen tcell.Screen) { if !t.GetVisible() { return } t.Box.Draw(screen) t.Lock() defer t.Unlock() if t.root == nil { return } t.process() // Scroll the tree. x, y, width, height := t.GetInnerRect() switch t.movement { case treeUp: t.offsetY-- case treeDown: t.offsetY++ case treeHome: t.offsetY = 0 case treeEnd: t.offsetY = len(t.nodes) case treePageUp: t.offsetY -= height case treePageDown: t.offsetY += height } t.movement = treeNone // Fix invalid offsets. if t.offsetY >= len(t.nodes)-height { t.offsetY = len(t.nodes) - height } if t.offsetY < 0 { t.offsetY = 0 } // Calculate scroll bar position. rows := len(t.nodes) cursor := int(float64(rows) * (float64(t.offsetY) / float64(rows-height))) // Draw the tree. posY := y lineStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.graphicsColor) for index, node := range t.nodes { // Skip invisible parts. if posY >= y+height { break } if index < t.offsetY { continue } // Draw the graphics. if t.graphics { // Draw ancestor branches. ancestor := node.parent for ancestor != nil && ancestor.parent != nil && ancestor.parent.level >= t.topLevel { if ancestor.graphicsX >= width { continue } // Draw a branch if this ancestor is not a last child. if ancestor.parent.children[len(ancestor.parent.children)-1] != ancestor { if posY-1 >= y && ancestor.textX > ancestor.graphicsX { PrintJoinedSemigraphics(screen, x+ancestor.graphicsX, posY-1, Borders.Vertical, t.graphicsColor) } if posY < y+height { screen.SetContent(x+ancestor.graphicsX, posY, Borders.Vertical, nil, lineStyle) } } ancestor = ancestor.parent } if node.textX > node.graphicsX && node.graphicsX < width { // Connect to the node above. if posY-1 >= y && t.nodes[index-1].graphicsX <= node.graphicsX && t.nodes[index-1].textX > node.graphicsX { PrintJoinedSemigraphics(screen, x+node.graphicsX, posY-1, Borders.TopLeft, t.graphicsColor) } // Join this node. if posY < y+height { screen.SetContent(x+node.graphicsX, posY, Borders.BottomLeft, nil, lineStyle) for pos := node.graphicsX + 1; pos < node.textX && pos < width; pos++ { screen.SetContent(x+pos, posY, Borders.Horizontal, nil, lineStyle) } } } } // Draw the prefix and the text. if node.textX < width && posY < y+height { // Prefix. var prefixWidth int if len(t.prefixes) > 0 { _, prefixWidth = Print(screen, t.prefixes[(node.level-t.topLevel)%len(t.prefixes)], x+node.textX, posY, width-node.textX, AlignLeft, node.color) } // Text. if node.textX+prefixWidth < width { style := tcell.StyleDefault.Foreground(node.color) if node == t.currentNode { backgroundColor := node.color foregroundColor := t.backgroundColor if t.selectedTextColor != nil { foregroundColor = *t.selectedTextColor } if t.selectedBackgroundColor != nil { backgroundColor = *t.selectedBackgroundColor } style = tcell.StyleDefault.Background(backgroundColor).Foreground(foregroundColor) } PrintStyle(screen, []byte(node.text), x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style) } } // Draw scroll bar. RenderScrollBar(screen, t.scrollBarVisibility, x+(width-1), posY, height, rows, cursor, posY-y, t.hasFocus, t.scrollBarColor) // Advance. posY++ } } // InputHandler returns the handler for this primitive. func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { selectNode := func() { t.Lock() currentNode := t.currentNode t.Unlock() if currentNode == nil { return } if t.selected != nil { t.selected(currentNode) } if currentNode.focused != nil { currentNode.focused() } if currentNode.selected != nil { currentNode.selected() } } t.Lock() defer t.Unlock() // Because the tree is flattened into a list only at drawing time, we also // postpone the (selection) movement to drawing time. if HitShortcut(event, Keys.Cancel, Keys.MovePreviousField, Keys.MoveNextField) { if t.done != nil { t.Unlock() t.done(event.Key()) t.Lock() } } else if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) { t.movement = treeHome } else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) { t.movement = treeEnd } else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MovePreviousField) { t.movement = treeUp } else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) { t.movement = treeDown } else if HitShortcut(event, Keys.MovePreviousPage) { t.movement = treePageUp } else if HitShortcut(event, Keys.MoveNextPage) { t.movement = treePageDown } else if HitShortcut(event, Keys.Select, Keys.Select2) { t.Unlock() selectNode() t.Lock() } t.process() }) } // MouseHandler returns the mouse handler for this primitive. func (t *TreeView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { x, y := event.Position() if !t.InRect(x, y) { return false, nil } switch action { case MouseLeftClick: _, rectY, _, _ := t.GetInnerRect() y -= rectY if y >= 0 && y < len(t.nodes) { node := t.nodes[y] if node.selectable { if t.currentNode != node && t.changed != nil { t.changed(node) } if t.selected != nil { t.selected(node) } t.currentNode = node } } consumed = true setFocus(t) case MouseScrollUp: t.movement = treeUp consumed = true case MouseScrollDown: t.movement = treeDown consumed = true } return }) }
package cview import ( "fmt" "math" "regexp" "sort" "strconv" "github.com/gdamore/tcell/v2" "github.com/mattn/go-runewidth" "github.com/rivo/uniseg" ) // ColorUnset represents an unset color. This is necessary because the zero // value of color, ColorDefault, results in default terminal colors. var ColorUnset = tcell.ColorSpecial | 108 // Text alignment within a box. const ( AlignLeft = iota AlignCenter AlignRight ) // Common regular expressions. var ( colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([bdilrsu]+|\-)?)?)?\]`) regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`) escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`) nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`) boundaryPattern = regexp.MustCompile(`(([,\.\-:;!\?&#+]|\n)[ \t\f\r]*|([ \t\f\r]+))`) spacePattern = regexp.MustCompile(`\s+`) ) // Positions of substrings in regular expressions. const ( colorForegroundPos = 1 colorBackgroundPos = 3 colorFlagPos = 5 ) // Predefined InputField acceptance functions. var ( // InputFieldInteger accepts integers. InputFieldInteger func(text string, ch rune) bool // InputFieldFloat accepts floating-point numbers. InputFieldFloat func(text string, ch rune) bool // InputFieldMaxLength returns an input field accept handler which accepts // input strings up to a given length. Use it like this: // // inputField.SetAcceptanceFunc(InputFieldMaxLength(10)) // Accept up to 10 characters. InputFieldMaxLength func(maxLength int) func(text string, ch rune) bool ) // Transformation describes a widget state modification. type Transformation int // Widget transformations. const ( TransformFirstItem Transformation = 1 TransformLastItem Transformation = 2 TransformPreviousItem Transformation = 3 TransformNextItem Transformation = 4 TransformPreviousPage Transformation = 5 TransformNextPage Transformation = 6 ) // Package initialization. func init() { runewidth.EastAsianWidth = true // Initialize the predefined input field handlers. InputFieldInteger = func(text string, ch rune) bool { if text == "-" { return true } _, err := strconv.Atoi(text) return err == nil } InputFieldFloat = func(text string, ch rune) bool { if text == "-" || text == "." || text == "-." { return true } _, err := strconv.ParseFloat(text, 64) return err == nil } InputFieldMaxLength = func(maxLength int) func(text string, ch rune) bool { return func(text string, ch rune) bool { return len([]rune(text)) <= maxLength } } } // StripTags returns the provided text without color and/or region tags. func StripTags(text []byte, colors bool, regions bool) []byte { if !colors && !regions { stripped := make([]byte, len(text)) copy(stripped, text) return stripped } var stripped []byte src := text if regions { stripped = regionPattern.ReplaceAll(text, nil) src = stripped } if colors { stripped = colorPattern.ReplaceAllFunc(src, func(match []byte) []byte { if len(match) > 2 { return nil } return match }) } return escapePattern.ReplaceAll(stripped, []byte(`[$1$2]`)) } // ColorHex returns the hexadecimal value of a color as a string, prefixed with #. // If the color is invalid, a blank string is returned. func ColorHex(c tcell.Color) string { if !c.Valid() { return "" } r, g, b := c.RGB() return fmt.Sprintf("#%02x%02x%02x", r, g, b) } // styleFromTag takes the given style, defined by a foreground color (fgColor), // a background color (bgColor), and style attributes, and modifies it based on // the substrings (tagSubstrings) extracted by the regular expression for color // tags. The new colors and attributes are returned where empty strings mean // "don't modify" and a dash ("-") means "reset to default". func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings [][]byte) (newFgColor, newBgColor, newAttributes string) { if len(tagSubstrings[colorForegroundPos]) > 0 { color := string(tagSubstrings[colorForegroundPos]) if color == "-" { fgColor = "-" } else if color != "" { fgColor = color } } if len(tagSubstrings[colorBackgroundPos-1]) > 0 { color := string(tagSubstrings[colorBackgroundPos]) if color == "-" { bgColor = "-" } else if color != "" { bgColor = color } } if len(tagSubstrings[colorFlagPos-1]) > 0 { flags := string(tagSubstrings[colorFlagPos]) if flags == "-" { attributes = "-" } else if flags != "" { attributes = flags } } return fgColor, bgColor, attributes } // overlayStyle mixes a background color with a foreground color (fgColor), // a (possibly new) background color (bgColor), and style attributes, and // returns the resulting style. For a definition of the colors and attributes, // see styleFromTag(). Reset instructions cause the corresponding part of the // default style to be used. func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgColor, attributes string) tcell.Style { defFg, defBg, defAttr := defaultStyle.Decompose() style := defaultStyle.Background(background) style = style.Foreground(defFg) if fgColor != "" { if fgColor == "-" { style = style.Foreground(defFg) } else { style = style.Foreground(tcell.GetColor(fgColor).TrueColor()) } } if bgColor == "-" || bgColor == "" && defBg != tcell.ColorDefault { style = style.Background(defBg) } else if bgColor != "" { style = style.Background(tcell.GetColor(bgColor).TrueColor()) } if attributes == "-" { style = style.Bold(defAttr&tcell.AttrBold > 0) style = style.Dim(defAttr&tcell.AttrDim > 0) style = style.Italic(defAttr&tcell.AttrItalic > 0) style = style.Blink(defAttr&tcell.AttrBlink > 0) style = style.Reverse(defAttr&tcell.AttrReverse > 0) style = style.StrikeThrough(defAttr&tcell.AttrStrikeThrough > 0) style = style.Underline(defAttr&tcell.AttrUnderline > 0) } else if attributes != "" { style = style.Normal() for _, flag := range attributes { switch flag { case 'b': style = style.Bold(true) case 'd': style = style.Dim(true) case 'i': style = style.Italic(true) case 'l': style = style.Blink(true) case 'r': style = style.Reverse(true) case 's': style = style.StrikeThrough(true) case 'u': style = style.Underline(true) } } } return style } // SetAttributes sets attributes on a style. func SetAttributes(style tcell.Style, attrs tcell.AttrMask) tcell.Style { return style. Bold(attrs&tcell.AttrBold != 0). Dim(attrs&tcell.AttrDim != 0). Italic(attrs&tcell.AttrItalic != 0). Blink(attrs&tcell.AttrBlink != 0). Reverse(attrs&tcell.AttrReverse != 0). StrikeThrough(attrs&tcell.AttrStrikeThrough != 0). Underline(attrs&tcell.AttrUnderline != 0) } // decomposeText returns information about a string which may contain color // tags or region tags, depending on which ones are requested to be found. It // returns the indices of the color tags (as returned by // re.FindAllStringIndex()), the color tags themselves (as returned by // re.FindAllStringSubmatch()), the indices of region tags and the region tags // themselves, the indices of an escaped tags (only if at least color tags or // region tags are requested), the string stripped by any tags and escaped, and // the screen width of the stripped string. func decomposeText(text []byte, findColors, findRegions bool) (colorIndices [][]int, colors [][][]byte, regionIndices [][]int, regions [][][]byte, escapeIndices [][]int, stripped []byte, width int) { // Shortcut for the trivial case. if !findColors && !findRegions { return nil, nil, nil, nil, nil, text, runewidth.StringWidth(string(text)) } // Get positions of any tags. if findColors { colorIndices = colorPattern.FindAllIndex(text, -1) colors = colorPattern.FindAllSubmatch(text, -1) } if findRegions { regionIndices = regionPattern.FindAllIndex(text, -1) regions = regionPattern.FindAllSubmatch(text, -1) } escapeIndices = escapePattern.FindAllIndex(text, -1) // Because the color pattern detects empty tags, we need to filter them out. for i := len(colorIndices) - 1; i >= 0; i-- { if colorIndices[i][1]-colorIndices[i][0] == 2 { colorIndices = append(colorIndices[:i], colorIndices[i+1:]...) colors = append(colors[:i], colors[i+1:]...) } } // Make a (sorted) list of all tags. var allIndices [][]int if findColors && findRegions { allIndices = colorIndices allIndices = make([][]int, len(colorIndices)+len(regionIndices)) copy(allIndices, colorIndices) copy(allIndices[len(colorIndices):], regionIndices) sort.Slice(allIndices, func(i int, j int) bool { return allIndices[i][0] < allIndices[j][0] }) } else if findColors { allIndices = colorIndices } else { allIndices = regionIndices } // Remove the tags from the original string. var from int buf := make([]byte, 0, len(text)) for _, indices := range allIndices { buf = append(buf, text[from:indices[0]]...) from = indices[1] } buf = append(buf, text[from:]...) // Escape string. stripped = escapePattern.ReplaceAll(buf, []byte("[$1$2]")) // Get the width of the stripped string. width = runewidth.StringWidth(string(stripped)) return } // Print prints text onto the screen into the given box at (x,y,maxWidth,1), // not exceeding that box. "align" is one of AlignLeft, AlignCenter, or // AlignRight. The screen's background color will not be changed. // // You can change the colors and text styles mid-text by inserting a color tag. // See the package description for details. // // Returns the number of actual bytes of the text printed (including color tags) // and the actual width used for the printed runes. func Print(screen tcell.Screen, text []byte, x, y, maxWidth, align int, color tcell.Color) (int, int) { return PrintStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color)) } // PrintStyle works like Print() but it takes a style instead of just a // foreground color. func PrintStyle(screen tcell.Screen, text []byte, x, y, maxWidth, align int, style tcell.Style) (int, int) { if maxWidth <= 0 || len(text) == 0 { return 0, 0 } // Decompose the text. colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeText(text, true, false) // We want to reduce all alignments to AlignLeft. if align == AlignRight { if strippedWidth <= maxWidth { // There's enough space for the entire text. return PrintStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style) } // Trim characters off the beginning. var ( bytes, width, colorPos, escapePos, tagOffset int foregroundColor, backgroundColor, attributes string ) _, originalBackground, _ := style.Decompose() iterateString(string(strippedText), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Update color/escape tag offset and style. if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] { foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes) tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0] colorPos++ } if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] { tagOffset++ escapePos++ } if strippedWidth-screenPos < maxWidth { // We chopped off enough. if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] { // Unescape open escape sequences. escapeCharPos := escapeIndices[escapePos-1][1] - 2 text = append(text[:escapeCharPos], text[escapeCharPos+1:]...) } // Print and return. bytes, width = PrintStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style) return true } return false }) return bytes, width } else if align == AlignCenter { if strippedWidth == maxWidth { // Use the exact space. return PrintStyle(screen, text, x, y, maxWidth, AlignLeft, style) } else if strippedWidth < maxWidth { // We have more space than we need. half := (maxWidth - strippedWidth) / 2 return PrintStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style) } else { // Chop off runes until we have a perfect fit. var choppedLeft, choppedRight, leftIndex, rightIndex int rightIndex = len(strippedText) for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth { if choppedLeft < choppedRight { // Iterate on the left by one character. iterateString(string(strippedText[leftIndex:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { choppedLeft += screenWidth leftIndex += textWidth return true }) } else { // Iterate on the right by one character. iterateStringReverse(string(strippedText[leftIndex:rightIndex]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { choppedRight += screenWidth rightIndex -= textWidth return true }) } } // Add tag offsets and determine start style. var ( colorPos, escapePos, tagOffset int foregroundColor, backgroundColor, attributes string ) _, originalBackground, _ := style.Decompose() for index := range strippedText { // We only need the offset of the left index. if index > leftIndex { // We're done. if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] { // Unescape open escape sequences. escapeCharPos := escapeIndices[escapePos-1][1] - 2 text = append(text[:escapeCharPos], text[escapeCharPos+1:]...) } break } // Update color/escape tag offset. if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] { if index <= leftIndex { foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes) } tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0] colorPos++ } if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] { tagOffset++ escapePos++ } } return PrintStyle(screen, text[leftIndex+tagOffset:], x, y, maxWidth, AlignLeft, style) } } // Draw text. var ( drawn, drawnWidth, colorPos, escapePos, tagOffset int foregroundColor, backgroundColor, attributes string ) iterateString(string(strippedText), func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool { // Only continue if there is still space. if drawnWidth+screenWidth > maxWidth { return true } // Handle color tags. for colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] { foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0] colorPos++ } // Handle scape tags. if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] { if textPos+tagOffset == escapeIndices[escapePos][1]-2 { tagOffset++ escapePos++ } } // Print the rune sequence. finalX := x + drawnWidth _, _, finalStyle, _ := screen.GetContent(finalX, y) _, background, _ := finalStyle.Decompose() finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes) for offset := screenWidth - 1; offset >= 0; offset-- { // To avoid undesired effects, we populate all cells. if offset == 0 { screen.SetContent(finalX+offset, y, main, comb, finalStyle) } else { screen.SetContent(finalX+offset, y, ' ', nil, finalStyle) } } // Advance. drawn += length drawnWidth += screenWidth return false }) return drawn + tagOffset + len(escapeIndices), drawnWidth } // PrintSimple prints white text to the screen at the given position. func PrintSimple(screen tcell.Screen, text []byte, x, y int) { Print(screen, text, x, y, math.MaxInt32, AlignLeft, Styles.PrimaryTextColor) } // TaggedTextWidth returns the width of the given string needed to print it on // screen. The text may contain color tags which are not counted. func TaggedTextWidth(text []byte) int { _, _, _, _, _, _, width := decomposeText(text, true, false) return width } // TaggedStringWidth returns the width of the given string needed to print it on // screen. The text may contain color tags which are not counted. func TaggedStringWidth(text string) int { return TaggedTextWidth([]byte(text)) } // WordWrap splits a text such that each resulting line does not exceed the // given screen width. Possible split points are after any punctuation or // whitespace. Whitespace after split points will be dropped. // // This function considers color tags to have no width. // // Text is always split at newline characters ('\n'). // // Text must not be escaped. func WordWrap(text string, width int) (lines []string) { colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeText([]byte(text), true, false) // Find candidate breakpoints. breakpoints := boundaryPattern.FindAllSubmatchIndex(strippedText, -1) // Results in one entry for each candidate. Each entry is an array a of // indices into strippedText where a[6] < 0 for newline/punctuation matches // and a[4] < 0 for whitespace matches. // Process stripped text one character at a time. var ( colorPos, escapePos, breakpointPos, tagOffset int lastBreakpoint, lastContinuation, currentLineStart int lineWidth, overflow int forceBreak bool ) unescape := func(substr string, startIndex int) string { // A helper function to unescape escaped tags. for index := escapePos; index >= 0; index-- { if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 { pos := escapeIndices[index][1] - 2 - startIndex if pos < 0 || pos > len(substr) { return substr } return substr[:pos] + substr[pos+1:] } } return substr } iterateString(string(strippedText), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Handle tags. for { if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { // Colour tags. tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] colorPos++ } else if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 { // Escape tags. tagOffset++ escapePos++ } else { break } } // Is this a breakpoint? if breakpointPos < len(breakpoints) && textPos+tagOffset == breakpoints[breakpointPos][0]+1 { // Yes, it is. Set up breakpoint infos depending on its type. lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset lastContinuation = breakpoints[breakpointPos][1] + tagOffset overflow = 0 forceBreak = main == '\n' if breakpoints[breakpointPos][6] < 0 && !forceBreak { lastBreakpoint++ // Don't skip punctuation. } breakpointPos++ } // Check if a break is warranted. if forceBreak || lineWidth > 0 && lineWidth+screenWidth > width { breakpoint := lastBreakpoint continuation := lastContinuation if forceBreak { breakpoint = textPos + tagOffset continuation = textPos + tagOffset + 1 lastBreakpoint = 0 overflow = 0 } else if lastBreakpoint <= currentLineStart { breakpoint = textPos + tagOffset continuation = textPos + tagOffset overflow = 0 } lines = append(lines, unescape(text[currentLineStart:breakpoint], currentLineStart)) currentLineStart, lineWidth, forceBreak = continuation, overflow, false } // Remember the characters since the last breakpoint. if lastBreakpoint > 0 && lastContinuation <= textPos+tagOffset { overflow += screenWidth } // Advance. lineWidth += screenWidth // But if we're still inside a breakpoint, skip next character (whitespace). if textPos+tagOffset < currentLineStart { lineWidth -= screenWidth } return false }) // Flush the rest. if currentLineStart < len(text) { lines = append(lines, unescape(text[currentLineStart:], currentLineStart)) } return } // EscapeBytes escapes the given text such that color and/or region tags are not // recognized and substituted by the print functions of this package. For // example, to include a tag-like string in a box title or in a TextView: // // box.SetTitle(cview.Escape("[squarebrackets]")) // fmt.Fprint(textView, cview.EscapeBytes(`["quoted"]`)) func EscapeBytes(text []byte) []byte { return nonEscapePattern.ReplaceAll(text, []byte("$1[]")) } // Escape escapes the given text such that color and/or region tags are not // recognized and substituted by the print functions of this package. For // example, to include a tag-like string in a box title or in a TextView: // // box.SetTitle(cview.Escape("[squarebrackets]")) // fmt.Fprint(textView, cview.Escape(`["quoted"]`)) func Escape(text string) string { return nonEscapePattern.ReplaceAllString(text, "$1[]") } // iterateString iterates through the given string one printed character at a // time. For each such character, the callback function is called with the // Unicode code points of the character (the first rune and any combining runes // which may be nil if there aren't any), the starting position (in bytes) // within the original string, its length in bytes, the screen position of the // character, and the screen width of it. The iteration stops if the callback // returns true. This function returns true if the iteration was stopped before // the last character. func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool { var screenPos int gr := uniseg.NewGraphemes(text) for gr.Next() { r := gr.Runes() from, to := gr.Positions() width := runewidth.StringWidth(gr.Str()) var comb []rune if len(r) > 1 { comb = r[1:] } if callback(r[0], comb, from, to-from, screenPos, width) { return true } screenPos += width } return false } // iterateStringReverse iterates through the given string in reverse, starting // from the end of the string, one printed character at a time. For each such // character, the callback function is called with the Unicode code points of // the character (the first rune and any combining runes which may be nil if // there aren't any), the starting position (in bytes) within the original // string, its length in bytes, the screen position of the character, and the // screen width of it. The iteration stops if the callback returns true. This // function returns true if the iteration was stopped before the last character. func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool { type cluster struct { main rune comb []rune textPos, textWidth, screenPos, screenWidth int } // Create the grapheme clusters. var clusters []cluster iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool { clusters = append(clusters, cluster{ main: main, comb: comb, textPos: textPos, textWidth: textWidth, screenPos: screenPos, screenWidth: screenWidth, }) return false }) // Iterate in reverse. for index := len(clusters) - 1; index >= 0; index-- { if callback( clusters[index].main, clusters[index].comb, clusters[index].textPos, clusters[index].textWidth, clusters[index].screenPos, clusters[index].screenWidth, ) { return true } } return false } // ScrollBarVisibility specifies the display of a scroll bar. type ScrollBarVisibility int const ( // ScrollBarNever never shows a scroll bar. ScrollBarNever ScrollBarVisibility = iota // ScrollBarAuto shows a scroll bar when there are items offscreen. ScrollBarAuto // ScrollBarAlways always shows a scroll bar. ScrollBarAlways ) // Scroll bar render text (must be one cell wide) var ( ScrollBarArea = []byte("[-:-:-]░") ScrollBarAreaFocused = []byte("[-:-:-]▒") ScrollBarHandle = []byte("[-:-:-]▓") ScrollBarHandleFocused = []byte("[::r] [-:-:-]") ) // RenderScrollBar renders a scroll bar at the specified position. func RenderScrollBar(screen tcell.Screen, visibility ScrollBarVisibility, x int, y int, height int, items int, cursor int, printed int, focused bool, color tcell.Color) { if visibility == ScrollBarNever || (visibility == ScrollBarAuto && items <= height) { return } // Place cursor at top when there are no items offscreen. if items <= height { cursor = 0 } // Handle negative cursor. if cursor < 0 { cursor = 0 } // Calculate handle position. handlePosition := int(float64(height-1) * (float64(cursor) / float64(items-1))) // Print scroll bar. var text []byte if printed == handlePosition { if focused { text = ScrollBarHandleFocused } else { text = ScrollBarHandle } } else { if focused { text = ScrollBarAreaFocused } else { text = ScrollBarArea } } Print(screen, text, x, y, 1, AlignLeft, color) }
package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // Window is a draggable, resizable frame around a primitive. Windows must be // added to a WindowManager. type Window struct { *Box primitive Primitive fullscreen bool normalX, normalY int normalW, normalH int dragX, dragY int dragWX, dragWY int sync.RWMutex } // NewWindow returns a new window around the given primitive. func NewWindow(primitive Primitive) *Window { w := &Window{ Box: NewBox(), primitive: primitive, dragWX: -1, dragWY: -1, } w.Box.focus = w return w } // SetFullscreen sets the flag indicating whether or not the the window should // be drawn fullscreen. func (w *Window) SetFullscreen(fullscreen bool) { w.Lock() defer w.Unlock() if w.fullscreen == fullscreen { return } w.fullscreen = fullscreen if w.fullscreen { w.normalX, w.normalY, w.normalW, w.normalH = w.GetRect() } else { w.SetRect(w.normalX, w.normalY, w.normalW, w.normalH) } } // Focus is called when this primitive receives focus. func (w *Window) Focus(delegate func(p Primitive)) { w.Lock() defer w.Unlock() w.Box.Focus(delegate) w.primitive.Focus(delegate) } // Blur is called when this primitive loses focus. func (w *Window) Blur() { w.Lock() defer w.Unlock() w.Box.Blur() w.primitive.Blur() } // HasFocus returns whether or not this primitive has focus. func (w *Window) HasFocus() bool { w.RLock() defer w.RUnlock() focusable := w.primitive.GetFocusable() if focusable != nil { return focusable.HasFocus() } return w.Box.HasFocus() } // Draw draws this primitive onto the screen. func (w *Window) Draw(screen tcell.Screen) { if !w.GetVisible() { return } w.RLock() defer w.RUnlock() w.Box.Draw(screen) x, y, width, height := w.GetInnerRect() w.primitive.SetRect(x, y, width, height) w.primitive.Draw(screen) } // InputHandler returns the handler for this primitive. func (w *Window) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return w.primitive.InputHandler() } // MouseHandler returns the mouse handler for this primitive. func (w *Window) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return w.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !w.InRect(event.Position()) { return false, nil } if action == MouseLeftDown || action == MouseMiddleDown || action == MouseRightDown { setFocus(w) } if action == MouseLeftDown { x, y, width, height := w.GetRect() mouseX, mouseY := event.Position() leftEdge := mouseX == x rightEdge := mouseX == x+width-1 bottomEdge := mouseY == y+height-1 topEdge := mouseY == y if mouseY >= y && mouseY <= y+height-1 { if leftEdge { w.dragX = -1 } else if rightEdge { w.dragX = 1 } } if mouseX >= x && mouseX <= x+width-1 { if bottomEdge { w.dragY = -1 } else if topEdge { if leftEdge || rightEdge { w.dragY = 1 } else { w.dragWX = mouseX - x w.dragWY = mouseY - y } } } } _, capture = w.primitive.MouseHandler()(action, event, setFocus) return true, capture }) }
package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // WindowManager provides an area which windows may be added to. type WindowManager struct { *Box windows []*Window sync.RWMutex } // NewWindowManager returns a new window manager. func NewWindowManager() *WindowManager { return &WindowManager{ Box: NewBox(), } } // Add adds a window to the manager. func (wm *WindowManager) Add(w ...*Window) { wm.Lock() defer wm.Unlock() for _, window := range w { window.SetBorder(true) } wm.windows = append(wm.windows, w...) } // Clear removes all windows from the manager. func (wm *WindowManager) Clear() { wm.Lock() defer wm.Unlock() wm.windows = nil } // Focus is called when this primitive receives focus. func (wm *WindowManager) Focus(delegate func(p Primitive)) { wm.Lock() defer wm.Unlock() if len(wm.windows) == 0 { return } wm.windows[len(wm.windows)-1].Focus(delegate) } // HasFocus returns whether or not this primitive has focus. func (wm *WindowManager) HasFocus() bool { wm.RLock() defer wm.RUnlock() for _, w := range wm.windows { if w.HasFocus() { return true } } return false } // Draw draws this primitive onto the screen. func (wm *WindowManager) Draw(screen tcell.Screen) { if !wm.GetVisible() { return } wm.RLock() defer wm.RUnlock() wm.Box.Draw(screen) x, y, width, height := wm.GetInnerRect() var hasFullScreen bool for _, w := range wm.windows { if !w.fullscreen || !w.GetVisible() { continue } hasFullScreen = true w.SetRect(x-1, y, width+2, height+1) w.Draw(screen) } if hasFullScreen { return } for _, w := range wm.windows { if !w.GetVisible() { continue } // Reposition out of bounds windows margin := 3 wx, wy, ww, wh := w.GetRect() ox, oy := wx, wy if wx > x+width-margin { wx = x + width - margin } if wx+ww < x+margin { wx = x - ww + margin } if wy > y+height-margin { wy = y + height - margin } if wy < y { wy = y // No top margin } if wx != ox || wy != oy { w.SetRect(wx, wy, ww, wh) } w.Draw(screen) } } // MouseHandler returns the mouse handler for this primitive. func (wm *WindowManager) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return wm.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if !wm.InRect(event.Position()) { return false, nil } if action == MouseMove { mouseX, mouseY := event.Position() for _, w := range wm.windows { if w.dragWX != -1 || w.dragWY != -1 { offsetX := w.x - mouseX offsetY := w.y - mouseY w.x -= offsetX + w.dragWX w.y -= offsetY + w.dragWY w.updateInnerRect() consumed = true } if w.dragX != 0 { if w.dragX == -1 { offsetX := w.x - mouseX if w.width+offsetX >= Styles.WindowMinWidth { w.x -= offsetX w.width += offsetX } } else { offsetX := mouseX - (w.x + w.width) + 1 if w.width+offsetX >= Styles.WindowMinWidth { w.width += offsetX } } w.updateInnerRect() consumed = true } if w.dragY != 0 { if w.dragY == -1 { offsetY := mouseY - (w.y + w.height) + 1 if w.height+offsetY >= Styles.WindowMinHeight { w.height += offsetY } } else { offsetY := w.y - mouseY if w.height+offsetY >= Styles.WindowMinHeight { w.y -= offsetY w.height += offsetY } } w.updateInnerRect() consumed = true } } } else if action == MouseLeftUp { for _, w := range wm.windows { w.dragX, w.dragY = 0, 0 w.dragWX, w.dragWY = -1, -1 } } // Focus window on mousedown var ( focusWindow *Window focusWindowIndex int ) for i := len(wm.windows) - 1; i >= 0; i-- { if wm.windows[i].InRect(event.Position()) { focusWindow = wm.windows[i] focusWindowIndex = i break } } if focusWindow != nil { if action == MouseLeftDown || action == MouseMiddleDown || action == MouseRightDown { for _, w := range wm.windows { if w != focusWindow { w.Blur() } } wm.windows = append(append(wm.windows[:focusWindowIndex], wm.windows[focusWindowIndex+1:]...), focusWindow) } return focusWindow.MouseHandler()(action, event, setFocus) } return consumed, nil }) }
// Package cpick is an interactive color picker in the terminal using cview package cpick import ( "fmt" color "github.com/ethanbaker/colors" "github.com/ethanbaker/cpick/cview" "github.com/gdamore/tcell/v2" ) // Default keys, runes, and mod masks for simplicity var dk tcell.Key = tcell.KeyF64 var dr rune = '_' var dm tcell.ModMask // Easy way to simulate a tcell event func simEvent(key tcell.Key, r rune, m tcell.ModMask) *tcell.EventKey { return tcell.NewEventKey(key, r, m) } // Keys that will be used commonly var enter tcell.Key = tcell.KeyEnter var escape tcell.Key = tcell.KeyEscape var tab tcell.Key = tcell.KeyTab var movementRunes = []rune{'l', 'h', 'j', 'k', 'G'} var movementKeys = []tcell.Key{tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown} // Tester funcion used to test functions used in cpick without having // to export them or use them in an interactive application. func tester() error { // Test screen setups hScreenSetup() svScreenSetup() // Test non-error returning functions testHelp() // Test error returning functions var errFuncs = [...]func() error{testColorPages, testHTable, testSVTable, testSearch, testInputCapture} for _, v := range errFuncs { err := v() if err != nil { return err } } _, err := Start(false) return err } func testColorPages() error { app.SetFocus(colorPages) // Test setup colorPageSetup() // Test done function colorPageDoneFunc(escape) colorPageDoneFunc(tab) // Test selected function colorPageSelectedFunc(0, 0) colorPageSelectedFunc(0, 3) // Test selection changed function for i := 0; i < len(colorInfo); i++ { colorInfo[i].table.Select(1, 1) } // Test capture handler err := testColorPageCaptureHandler() if err != nil { return err } // Test color name getter function var hsvs = [...]color.HSV{{H: 0, S: 100, V: 100}, {H: 0, S: 100, V: 99}} var altHsvs = [...]color.HSV{{H: 0, S: 100, V: 99}, {H: 0, S: 100, V: 98}} for i := 0; i < len(hsvs); i++ { name := getColorName(hsvs[i], altHsvs[i]) switch i { case 0: if name != "red" { return fmt.Errorf(fmt.Sprintf("Error! getColorName(%v, %v) is not properly returning red!\nOutput: %v\n", hsvs[i], altHsvs[i], name)) } case 1: if name != "custom color" { return fmt.Errorf(fmt.Sprintf("Error! getColorName(%v, %v) is not properly returning custom color!\nOutput: %v\n", hsvs[i], altHsvs[i], name)) } } } // Test path getter function _, err = getPath() if err != nil { return err } // Test colors getter function var paths = [...]string{"", "./testing/colors.json"} for _, v := range paths { data := getCustomColors(v) if data.COLORLIST[0].NAME != "css" { return fmt.Errorf(fmt.Sprintf("Error! getCustomColors(%v) is not properly returning presetData!\nOutput: %v\n", v, data)) } } return nil } // Test capture handler func testColorPageCaptureHandler() error { var eventRunes = [...]rune{'C', 'c', 'C', 'c', ' ', 'n', 'N'} for i, v := range eventRunes { setEvent := simEvent(dk, v, dm) returnEvent := colorPageCaptureHandler(setEvent) switch i { case 0: colorInfo[colorPageIndex].table.Select(3, 1) case 1: colorInfo[colorPageIndex].table.Select(0, 0) } if setEvent != returnEvent { return fmt.Errorf(fmt.Sprintf("Error! colorPageCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", setEvent, returnEvent)) } } for _, v := range movementRunes { setEvent := simEvent(dk, v, dm) returnEvent := colorPageCaptureHandler(setEvent) if v == 'G' { if returnEvent != nil { return fmt.Errorf(fmt.Sprintf("Error! colorPageCaptureHandler(%v) is not properly returning nil for movement rune 'G'!\nOutput: %v\n", setEvent, returnEvent)) } } else { if returnEvent != setEvent { return fmt.Errorf(fmt.Sprintf("Error! colorPageCaptureHandler(%v) is not properly returning nil for movement runes!\nOutput: %v\n", setEvent, returnEvent)) } } } colorInfo[0].table.Select(1, 1) for _, v := range movementKeys { setEvent := simEvent(v, dr, dm) returnEvent := colorPageCaptureHandler(setEvent) if v == 'G' { if returnEvent != nil { return fmt.Errorf(fmt.Sprintf("Error! colorPageCaptureHandler(%v) is not properly returning nil for movement rune 'G'!\nOutput: %v\n", setEvent, returnEvent)) } } else { if returnEvent != setEvent { return fmt.Errorf(fmt.Sprintf("Error! colorPageCaptureHandler(%v) is not properly returning nil for movement runes!\nOutput: %v\n", setEvent, returnEvent)) } } } return nil } func testHTable() error { app.SetFocus(hTable) // Test setup function hTableSetup() // Test done function hTableDoneFunc(escape) hTableDoneFunc(tab) // Test selected function hTableSelectedFunc(0, 0) // Test selection changed function hTableSelectionChangedFunc(0, 0) // Test capture handler for i := 0; i < 2; i++ { setEvent := simEvent(dk, ' ', dm) returnEvent := hCaptureHandler(setEvent) if setEvent != returnEvent { return fmt.Errorf(fmt.Sprintf("Error! hCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", setEvent, returnEvent)) } colorInfo[0].table.Select(0, 1) } return nil } func testSVTable() error { app.SetFocus(svTable) // Test setup function svTableSetup() // Test done function svTableDoneFunc(escape) svTableDoneFunc(tab) // Test selected function svTableSelectedFunc(0, 0) // Test selection changed function svTableSelectionChangedFunc(0, 0) svTableSelectionChangedFunc(50, 0) // Test capture handler setEvent := simEvent(dk, dr, dm) returnEvent := svCaptureHandler(setEvent) if setEvent != returnEvent { return fmt.Errorf(fmt.Sprintf("Error! svCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", setEvent, returnEvent)) } // Test draw function drawSVTable() // Test block drawing darkHSV1 := color.HSV{H: 0, S: 101, V: 101} darkHSV2 := color.HSV{H: -1, S: -1, V: -1} lightHSV1 := color.HSV{H: 0, S: 101, V: 101} lightHSV2 := color.HSV{H: -1, S: -1, V: -1} setColorValues(darkHSV1, darkSVBlock, darkSVText, lightHSV1, lightSVBlock, lightSVText) setColorValues(darkHSV2, darkSVBlock, darkSVText, lightHSV2, lightSVBlock, lightSVText) return nil } func testHelp() { app.SetFocus(helpModal) // Test setup function helpPageSetup() var primitives = [...]cview.Primitive{colorPages, hTable, svTable} // Test done function and show help function for _, v := range primitives { helpFocus = v helpModalDoneFunc(0, "Exit help") app.SetFocus(v) showHelp() } } func testSearch() error { app.SetFocus(searchInput) // Test setup function searchInputSetup() // Test done function searchInputDoneFunc(escape) searchInput.SetText("red") searchInputDoneFunc(enter) // Test autocomplete function searchInputAutocompleteFunc("") searchInputAutocompleteFunc("red") searchInputAutocompleteFunc("lightgoldenrodyellow") searchInputAutocompleteFunc("?") // Test parsing function parseSearchText("#ffffff") parseSearchText("#fffffff") parseSearchText("rgb:a") parseSearchText("rgb:0 0 0") parseSearchText("rgb:0 0 -1") parseSearchText("rgb:0 0 0 0") parseSearchText("hsv:0 0 0") parseSearchText("hsv:-1 0 0") parseSearchText("hsv:0 0 -1") parseSearchText("hsv:0 0 0 0") parseSearchText("hsl:0 0 0") parseSearchText("hsl:-1 0 0") parseSearchText("hsl:0 0 -1") parseSearchText("hsl:0 0 0 0") parseSearchText("cmyk: 0 0 0 0") parseSearchText("cmyk: 0 0 0 -1") parseSearchText("cmyk: 0 0 0 0 0") parseSearchText("decimal: 0") parseSearchText("decimal: -1") parseSearchText("decimal: 0 0") parseSearchText("ansi:a") // Test capture handler var eventRunes = [...]rune{'n', 'N'} for i, v := range eventRunes { searchIndexes = [][]int{{0, 0, 0}, {1, 1, 1}} switch i { case 0: searchIndex = 0 case 1: searchIndex = len(searchIndexes) - 1 } setEvent := simEvent(dk, v, dm) returnEvent := searchInputCaptureHandler(setEvent) if setEvent != returnEvent { return fmt.Errorf(fmt.Sprintf("Error! searchInputCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", setEvent, returnEvent)) } } return nil } func testInputCapture() error { var eventKeys = [...]rune{'q', 'q', '`', '?'} for i, v := range eventKeys { switch i { case 1: app.SetFocus(searchInput) case 3: app.SetFocus(svTable) } setEvent := simEvent(dk, v, dm) returnEvent := inputCaptureHandler(setEvent) if i == 3 { setEvent = nil } if setEvent != returnEvent { return fmt.Errorf(fmt.Sprintf("Error! inputCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", simEvent(dk, v, dm), returnEvent)) } } var primitives = [...]cview.Primitive{hTable, colorPages, svTable} for _, v := range primitives { app.SetFocus(v) setEvent := simEvent(dk, dr, dm) returnEvent := inputCaptureHandler(setEvent) if setEvent != returnEvent { return fmt.Errorf(fmt.Sprintf("Error! inputCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", simEvent(dk, dr, dm), returnEvent)) } } return nil }