Skip to content

Commit

Permalink
Multiline text input boxes
Browse files Browse the repository at this point in the history
No doubt a lot of this code is reinventing the wheel, after all this is
basically now a simple text editor. I'm sure there'll be a few bugs to
iron out.
  • Loading branch information
tombh committed Jun 2, 2018
1 parent b78abbf commit 55c70d9
Show file tree
Hide file tree
Showing 11 changed files with 686 additions and 209 deletions.
4 changes: 1 addition & 3 deletions interfacer/src/browsh/frame_builder.go
Expand Up @@ -149,9 +149,7 @@ func (f *frame) updateInputBoxes(incoming incomingFrameText) {
}
for _, incomingInputBox := range incoming.InputBoxes {
if _, ok := f.inputBoxes[incomingInputBox.ID]; !ok {
f.inputBoxes[incomingInputBox.ID] = &inputBox{
ID: incomingInputBox.ID,
}
f.inputBoxes[incomingInputBox.ID] = newInputBox(incomingInputBox.ID)
}
inputBox := f.inputBoxes[incomingInputBox.ID]
inputBox.X = incomingInputBox.X
Expand Down
247 changes: 80 additions & 167 deletions interfacer/src/browsh/input_box.go
Expand Up @@ -7,9 +7,7 @@ import (
"github.com/gdamore/tcell"
)

var (
activeInputBox *inputBox
)
var activeInputBox *inputBox

// A box into which you can enter text. Generally will be forwarded to a standard
// HTML input box in the real browser.
Expand All @@ -30,14 +28,24 @@ type inputBox struct {
FgColour [3]int32 `json:"colour"`
bgColour [3]int32
isActive bool
multiLiner multiLine
text string
textLines []string
xCursor int
yCursor int
textCursor int
xScroll int
yScroll int
}

func newInputBox(id string) *inputBox {
newInputBox := &inputBox{
ID: id,
}
// TODO: Circular reference, what's the proper Golang way to do this?
newInputBox.multiLiner.inputBox = newInputBox
return newInputBox
}

// This is used only for the URL input box
func (i *inputBox) renderURLBox() {
bgRGB := tcell.ColorDefault
Expand All @@ -49,49 +57,74 @@ func (i *inputBox) renderURLBox() {
screen.SetContent(x, i.Y, c, nil, style)
x++
}
i.setCursor()
i.renderCursor()
screen.Show()
}

// This is used for all input boxes in the frame
func (i *inputBox) setCells() {
if i == nil { return }
i.resetCells()
x := i.X
y := i.Y
lineCount := 0
for _, c := range i.textToDisplay() {
if i.isMultiLine() && lineCount < i.yScroll {
if isLineBreak(string(c)) { lineCount++ }
continue
}
i.addCharacterToFrame(x, y, c)
x++
if i.isMultiLine() && isLineBreak(string(c)) {
x = i.X
y++
lineCount++
if lineCount - i.yScroll > i.Height { break }
}
}
screen.Show()
}

func (i *inputBox) resetCells() {
for y := i.Y; y < i.Height; y++ {
for x := i.X; x < i.Width; x++ {
i.addCharacterToFrame(x, y, ' ')
}
}
}

func (i *inputBox) addCharacterToFrame(x int, y int, c rune) {
var (
index int
inputBoxCell, existingCell cell
cellFGColour, cellBGColour tcell.Color
ok bool
)
x := i.X
y := i.Y
cellFGColour = tcell.NewRGBColor(i.FgColour[0], i.FgColour[1], i.FgColour[2])
for _, c := range i.textToDisplay() {
y = i.Y
index = (y * CurrentTab.frame.totalWidth) + x
if existingCell, ok = CurrentTab.frame.cells.load(index); ok {
cellBGColour = existingCell.bgColour
} else {
continue
}
inputBoxCell = cell{
character: []rune{c},
fgColour: cellFGColour,
bgColour: cellBGColour,
}
CurrentTab.frame.cells.store(index, inputBoxCell)
x++
index = (y * CurrentTab.frame.totalWidth) + x
if existingCell, ok = CurrentTab.frame.cells.load(index); ok {
cellBGColour = existingCell.bgColour
} else {
return
}
inputBoxCell = cell{
character: []rune{c},
fgColour: cellFGColour,
bgColour: cellBGColour,
}
CurrentTab.frame.cells.store(index, inputBoxCell)
}

// Different methods are used for containing and displaying overflowed text depending on the
// size of the input box.
func (i *inputBox) isMultiLine() bool {
return i.TagName == "textarea"
if urlInputBox.isActive { return false }
return i.TagName == "TEXTAREA"
}

func (i *inputBox) textToDisplay() []rune {
if i.isMultiLine() {
return i.textToDisplayForMultiLine()
return i.multiLiner.convert()
}
return i.textToDisplayForSingleLine()
}
Expand All @@ -109,143 +142,12 @@ func (i *inputBox) textToDisplayForSingleLine() []rune {
return []rune(textToDisplay)
}

func (i *inputBox) textToDisplayForMultiLine() []rune {
return []rune{'!'}
}

func (i *inputBox) setCursor() {
xFrameOffset := CurrentTab.frame.xScroll
yFrameOffset := CurrentTab.frame.yScroll - uiHeight
if urlInputBox.isActive {
xFrameOffset = 0
yFrameOffset = 0
}
x := (i.X + i.xCursor) - i.xScroll - xFrameOffset
y := (i.Y + i.yCursor) - i.yScroll - yFrameOffset
mainRune, combiningRunes, style, _ := screen.GetContent(x, y)
style = style.Reverse(true)
screen.SetContent(x, y, mainRune, combiningRunes, style)
}

func (i *inputBox) cursorLeft() {
i.xCursor--
if (i.xCursor - i.xScroll == -1) { i.xScrollBy(-1) }
i.limitCursor()
}

func (i *inputBox) cursorRight() {
i.xCursor++
i.limitCursor()
if (i.xCursor - i.xScroll == i.Width) { i.xScrollBy(1) }
}

func (i *inputBox) cursorUp() {
i.yCursor--
i.limitCursor()
if (i.yCursor - i.yScroll == 0) { i.yScrollBy(-1) }
}

func (i *inputBox) cursorDown() {
i.yCursor++
i.limitCursor()
if (i.yCursor - i.yScroll >= i.Height) { i.yScrollBy(1) }
}

func (i *inputBox) cursorBackspace() {
if (utf8.RuneCountInString(i.text) == 0) { return }
if (i.xCursor == 0) { return }
start := i.text[:i.xCursor - 1]
end := i.text[i.xCursor:]
i.text = start + end
i.xCursor--
i.limitCursor()
i.xScrollBy(-1)
i.sendInputBoxToBrowser()
}

func (i *inputBox) cursorInsertRune(theRune rune) {
character := string(theRune)
start := i.text[:i.xCursor]
end := i.text[i.xCursor:]
i.text = start + character + end
i.xCursor++
i.limitCursor()
i.xScrollBy(1)
i.sendInputBoxToBrowser()
}

func (i *inputBox) xScrollBy(magnitude int) {
detectionTextWidth := utf8.RuneCountInString(i.text)
detectionBoxWidth := i.Width
if !i.isMultiLine() {
if magnitude < 0 {
detectionTextWidth++
detectionBoxWidth -= 2
}
isOverflowing := detectionTextWidth >= i.Width
isCursorAtStartOfBox := i.xCursor - i.xScroll < 0
isCursorAtEndOfBox := i.xCursor - i.xScroll == detectionBoxWidth
isCursorAtEdgeOfBox := isCursorAtStartOfBox || isCursorAtEndOfBox
if isOverflowing && isCursorAtEdgeOfBox {
i.xScroll += magnitude
}
} else {
i.yScroll += magnitude
}
i.limitScroll()
}

func (i *inputBox) yScrollBy(magnitude int) {
if i.isMultiLine() {
i.yScroll += magnitude
}
i.limitScroll()
}

func (i *inputBox) putCursorAtEnd() {
i.xCursor = utf8.RuneCountInString(urlInputBox.text)
}

// Not that distinct methods are used for single line and multiline overflow, so their
// respective limit checks never encroach on each other.
func (i *inputBox) limitScroll() {
if (i.xScroll < 0) {
i.xScroll = 0
}
if (i.xScroll > utf8.RuneCountInString(i.text)) {
i.xScroll = utf8.RuneCountInString(i.text)
}
if (i.yScroll < 0) {
i.yScroll = 0
}
if (i.yScroll > i.textLineCount()) {
i.yScroll = i.textLineCount()
}
}

func (i *inputBox) textLineCount() int {
return len(i.textLines)
func (i *inputBox) lineCount() int {
return len(i.multiLiner.finalText)
}

func (i *inputBox) limitCursor() {
var upperXLimit int
if (i.xCursor < 0) {
i.xCursor = 0
}
if (i.isMultiLine()) {
upperXLimit = i.Width
} else {
upperXLimit = utf8.RuneCountInString(i.text)
}
if (i.xCursor > upperXLimit) {
i.xCursor = upperXLimit
}
if (i.yCursor < 0) {
i.yCursor = 0
}
if (i.yCursor > i.Height - 1) {
i.yCursor = i.Height - 1
}
func isLineBreak(character string) bool {
return character == "\n" || character == "\r"
}

func (i *inputBox) sendInputBoxToBrowser() {
Expand All @@ -257,23 +159,34 @@ func (i *inputBox) sendInputBoxToBrowser() {
sendMessageToWebExtension("/tab_command,/input_box," + string(marshalled))
}

func (i *inputBox) handleEnterKey() {
if urlInputBox.isActive {
if isNewEmptyTabActive() {
sendMessageToWebExtension("/new_tab," + i.text)
} else {
sendMessageToWebExtension("/url_bar," + i.text)
}
urlBarFocus(false)
}
if i.isMultiLine() {
i.cursorInsertRune([]rune("\n")[0])
}
}

func handleInputBoxInput(ev *tcell.EventKey) {
switch ev.Key() {
case tcell.KeyLeft:
activeInputBox.cursorLeft()
case tcell.KeyRight:
activeInputBox.cursorRight()
case tcell.KeyDown:
activeInputBox.cursorDown()
case tcell.KeyUp:
activeInputBox.cursorUp()
case tcell.KeyBackspace, tcell.KeyBackspace2:
activeInputBox.cursorBackspace()
case tcell.KeyEnter:
if urlInputBox.isActive {
if isNewEmptyTabActive() {
sendMessageToWebExtension("/new_tab," + activeInputBox.text)
} else {
sendMessageToWebExtension("/url_bar," + activeInputBox.text)
}
urlBarFocus(false)
}
activeInputBox.handleEnterKey()
case tcell.KeyRune:
activeInputBox.cursorInsertRune(ev.Rune())
}
Expand Down

0 comments on commit 55c70d9

Please sign in to comment.