diff options
Diffstat (limited to 'ui')
-rw-r--r-- | ui/controls/Form.js | 21 | ||||
-rw-r--r-- | ui/controls/ListScrollForm.js | 153 | ||||
-rw-r--r-- | ui/controls/ScrollBar.js | 121 | ||||
-rw-r--r-- | ui/controls/index.js | 2 | ||||
-rw-r--r-- | ui/presentation/Label.js | 17 | ||||
-rw-r--r-- | ui/presentation/WrapLabel.js | 8 | ||||
-rw-r--r-- | ui/primitives/DisplayElement.js | 6 | ||||
-rw-r--r-- | ui/primitives/Element.js | 2 |
8 files changed, 190 insertions, 140 deletions
diff --git a/ui/controls/Form.js b/ui/controls/Form.js index 921096a..0224247 100644 --- a/ui/controls/Form.js +++ b/ui/controls/Form.js @@ -81,8 +81,10 @@ export default class Form extends FocusElement { } previousInput() { - // TODO: Forms currently assume there is at least one selectable input, - // but this isn't necessarily always the case. + if (this.inputs.length === 0) { + return + } + do { this.curIndex = (this.curIndex - 1) if (this.curIndex < 0) { @@ -94,7 +96,10 @@ export default class Form extends FocusElement { } nextInput() { - // TODO: See previousInput + if (this.inputs.length === 0) { + return + } + do { this.curIndex = (this.curIndex + 1) % this.inputs.length } while (!this.inputs[this.curIndex].selectable) @@ -103,9 +108,12 @@ export default class Form extends FocusElement { } firstInput(selectForm = true) { + if (this.inputs.length === 0) { + return + } + this.curIndex = 0 - // TODO: See previousInput if (!this.inputs[this.curIndex].selectable) { this.nextInput() } @@ -118,9 +126,12 @@ export default class Form extends FocusElement { } lastInput(selectForm = true) { + if (this.inputs.length === 0) { + return + } + this.curIndex = this.inputs.length - 1 - // TODO: See previousInput if (!this.inputs[this.curIndex].selectable) { this.previousInput() } diff --git a/ui/controls/ListScrollForm.js b/ui/controls/ListScrollForm.js index 3f75599..f74561e 100644 --- a/ui/controls/ListScrollForm.js +++ b/ui/controls/ListScrollForm.js @@ -2,9 +2,8 @@ import * as ansi from 'tui-lib/util/ansi' import telc from 'tui-lib/util/telchars' import unic from 'tui-lib/util/unichars' -import {DisplayElement} from 'tui-lib/ui/primitives' - import Form from './Form.js' +import ScrollBar from './ScrollBar.js' export default class ListScrollForm extends Form { // A form that lets the user scroll through a list of items. It @@ -17,12 +16,18 @@ export default class ListScrollForm extends Form { super() this.layoutType = layoutType + this.wheelMode = 'scroll' // scroll, selection this.scrollItems = 0 this.scrollBarEnabled = enableScrollBar - this.scrollBar = new ScrollBar(this) + this.scrollBar = new ScrollBar({ + getLayoutType: () => this.layoutType, + getCurrentScroll: () => this.scrollItems, + getMaximumScroll: () => this.getScrollItemsLength(), + getTotalItems: () => this.inputs.length + }) this.scrollBarShown = false } @@ -119,27 +124,26 @@ export default class ListScrollForm extends Form { } clicked(button) { - // Old code for changing the actual selected item...maybe an interesting - // functionality to explore later? - /* - if (button === 'scroll-up') { - this.previousInput() - this.scrollSelectedElementIntoView() - } else if (button === 'scroll-down') { - this.nextInput() - this.scrollSelectedElementIntoView() - } - */ - - // Scrolling is typically pretty slow with a mouse wheel when it's by - // a single line, so scroll at 3x that speed. - for (let i = 0; i < 3; i++) { + if (this.wheelMode === 'selection') { + // Change the actual selected item. if (button === 'scroll-up') { - this.scrollItems-- + this.previousInput() + this.scrollSelectedElementIntoView() } else if (button === 'scroll-down') { - this.scrollItems++ - } else { - return + this.nextInput() + this.scrollSelectedElementIntoView() + } + } else if (this.wheelMode === 'scroll') { + // Scrolling is typically pretty slow with a mouse wheel when it's by + // a single line, so scroll at 3x that speed. + for (let i = 0; i < 3; i++) { + if (button === 'scroll-up') { + this.scrollItems-- + } else if (button === 'scroll-down') { + this.scrollItems++ + } else { + return + } } } @@ -298,108 +302,3 @@ export default class ListScrollForm extends Form { } } } - -class ScrollBar extends DisplayElement { - constructor(listScrollForm) { - super() - - this.listScrollForm = listScrollForm - } - - fixLayout() { - // Normally we'd subtract one from contentW/contentH when setting the x/y - // position, but the scrollbar is actually displayed OUTSIDE of (adjacent - // to) the parent's content area. - if (this.listScrollForm.layoutType === 'vertical') { - this.h = this.listScrollForm.contentH - this.w = 1 - this.x = this.listScrollForm.contentW - this.y = 0 - } else { - this.h = 1 - this.w = this.listScrollForm.contentW - this.x = 0 - this.y = this.listScrollForm.contentH - } - } - - drawTo(writable) { - // Uuuurgh - this.fixLayout() - - // TODO: Horizontal layout! Not functionally a lot different, but I'm too - // lazy to write a test UI for it right now. - - const { - backwards: canScrollBackwards, - forwards: canScrollForwards - } = this.getScrollableDirections() - - // - 2 for extra UI elements (arrows) - const totalLength = this.h - 2 - - // ..[-----].. - // ^start| - // ^end - // - // Start and end should correspond to how much of the scroll area - // is currently visible. So, if you can see 60% of the full scroll length - // at a time, and you are scrolled 10% down, the start position of the - // handle should be 10% down, and it should extend 60% of the scrollbar - // length, to the 70% mark. - - const currentScroll = this.listScrollForm.scrollItems - const edgeLength = this.listScrollForm.contentH - const totalItems = this.listScrollForm.inputs.length - const itemsVisibleAtOnce = Math.min(totalItems, edgeLength) - const handleLength = itemsVisibleAtOnce / totalItems * totalLength - let handlePosition = Math.floor(totalLength / totalItems * currentScroll) - - // Silly peeve of mine: The handle should only be visibly touching the top - // or bottom of the scrollbar area if you're actually scrolled all the way - // to the start or end. Otherwise, it shouldn't be touching! There should - // visible space indicating that you can scroll in that direction - // (in addition to the arrows we show at the ends). - - if (canScrollBackwards && handlePosition === 0) { - handlePosition = 1 - } - - if (canScrollForwards && (handlePosition + handleLength) === edgeLength) { - handlePosition-- - } - - if (this.listScrollForm.layoutType === 'vertical') { - const start = this.absTop + handlePosition + 1 - for (let i = 0; i < handleLength; i++) { - writable.write(ansi.moveCursor(start + i, this.absLeft)) - writable.write(unic.BOX_V_DOUBLE) - } - - if (canScrollBackwards) { - writable.write(ansi.moveCursor(this.absTop, this.absLeft)) - writable.write(unic.ARROW_UP_DOUBLE) - } - - if (canScrollForwards) { - writable.write(ansi.moveCursor(this.absBottom, this.absLeft)) - writable.write(unic.ARROW_DOWN_DOUBLE) - } - } - } - - getScrollableDirections() { - const currentScroll = this.listScrollForm.scrollItems - const totalScroll = this.listScrollForm.getScrollItemsLength() - - return { - backwards: (currentScroll > 0), - forwards: (currentScroll < totalScroll) - } - } - - canScrollAtAll() { - const {backwards, forwards} = this.getScrollableDirections() - return backwards || forwards - } -} diff --git a/ui/controls/ScrollBar.js b/ui/controls/ScrollBar.js new file mode 100644 index 0000000..4b79d57 --- /dev/null +++ b/ui/controls/ScrollBar.js @@ -0,0 +1,121 @@ +import * as ansi from 'tui-lib/util/ansi' +import unic from 'tui-lib/util/unichars' + +import {DisplayElement} from 'tui-lib/ui/primitives' + +export default class ScrollBar extends DisplayElement { + constructor({ + getLayoutType, + getCurrentScroll, + getMaximumScroll, + getTotalItems + }) { + super() + + this.getLayoutType = getLayoutType + this.getCurrentScroll = getCurrentScroll + this.getMaximumScroll = getMaximumScroll + this.getTotalItems = getTotalItems + } + + fixLayout() { + // Normally we'd subtract one from contentW/contentH when setting the x/y + // position, but the scroll-bar is actually displayed OUTSIDE of (adjacent + // to) the parent's content area. + if (this.getLayoutType() === 'vertical') { + this.h = this.parent.contentH + this.w = 1 + this.x = this.parent.contentW + this.y = 0 + } else { + this.h = 1 + this.w = this.parent.contentW + this.x = 0 + this.y = this.parent.contentH + } + } + + drawTo(writable) { + // Uuuurgh + this.fixLayout() + + // TODO: Horizontal layout! Not functionally a lot different, but I'm too + // lazy to write a test UI for it right now. + + const { + backwards: canScrollBackwards, + forwards: canScrollForwards + } = this.getScrollableDirections() + + // - 2 for extra UI elements (arrows) + const totalLength = this.h - 2 + + // ..[-----].. + // ^start| + // ^end + // + // Start and end should correspond to how much of the scroll area + // is currently visible. So, if you can see 60% of the full scroll length + // at a time, and you are scrolled 10% down, the start position of the + // handle should be 10% down, and it should extend 60% of the scrollbar + // length, to the 70% mark. + + // NB: I think this math mixes the units for "items" and "lines". + // edgeLength is measured in lines, while totalItems is a number of items. + // This isn't a problem when the length of an item is equal to one line, + // but it's still worth investigating at some point. + const currentScroll = this.getCurrentScroll() + const totalItems = this.getTotalItems() + const edgeLength = this.parent.contentH + const visibleAtOnce = Math.min(totalItems, edgeLength) + const handleLength = visibleAtOnce / totalItems * totalLength + let handlePosition = Math.floor(totalLength / totalItems * currentScroll) + + // Silly peeve of mine: The handle should only be visibly touching the top + // or bottom of the scrollbar area if you're actually scrolled all the way + // to the start or end. Otherwise, it shouldn't be touching! There should + // visible space indicating that you can scroll in that direction + // (in addition to the arrows we show at the ends). + + if (canScrollBackwards && handlePosition === 0) { + handlePosition = 1 + } + + if (canScrollForwards && (handlePosition + handleLength) === edgeLength) { + handlePosition-- + } + + if (this.getLayoutType() === 'vertical') { + const start = this.absTop + handlePosition + 1 + for (let i = 0; i < handleLength; i++) { + writable.write(ansi.moveCursor(start + i, this.absLeft)) + writable.write(unic.BOX_V_DOUBLE) + } + + if (canScrollBackwards) { + writable.write(ansi.moveCursor(this.absTop, this.absLeft)) + writable.write(unic.ARROW_UP_DOUBLE) + } + + if (canScrollForwards) { + writable.write(ansi.moveCursor(this.absBottom, this.absLeft)) + writable.write(unic.ARROW_DOWN_DOUBLE) + } + } + } + + getScrollableDirections() { + const currentScroll = this.getCurrentScroll() + const maximumScroll = this.getMaximumScroll() + + return { + backwards: (currentScroll > 0), + forwards: (currentScroll < maximumScroll) + } + } + + canScrollAtAll() { + const {backwards, forwards} = this.getScrollableDirections() + return backwards || forwards + } +} diff --git a/ui/controls/index.js b/ui/controls/index.js index e99add1..7f290c2 100644 --- a/ui/controls/index.js +++ b/ui/controls/index.js @@ -4,12 +4,14 @@ // primitives -> // Button // FocusBox +// ScrollBar // TextInput // // Form -> ListScrollForm // export {default as Button} from './Button.js' +export {default as ScrollBar} from './ScrollBar.js' export {default as FocusBox} from './FocusBox.js' export {default as Form} from './Form.js' export {default as ListScrollForm} from './ListScrollForm.js' diff --git a/ui/presentation/Label.js b/ui/presentation/Label.js index 81223df..ed45601 100644 --- a/ui/presentation/Label.js +++ b/ui/presentation/Label.js @@ -4,6 +4,16 @@ import * as ansi from 'tui-lib/util/ansi' export default class Label extends DisplayElement { // A simple text display. Automatically adjusts size to fit text. + // + // Supports formatted text in two ways: + // 1) Modify the textAttributes to be an array containing the ANSI numerical + // codes for any wanted attributes, and/or + // 2) Supply full ANSI escape codes within the text itself. (The reset + // attributes code, ESC[0m, will be processed to reset to the provided + // values in textAttributes. + // + // Subclasses overriding the writeTextTo function should be sure to call + // processFormatting before actually writing text. constructor(text = '') { super() @@ -32,7 +42,12 @@ export default class Label extends DisplayElement { writeTextTo(writable) { writable.write(ansi.moveCursor(this.absTop, this.absLeft)) - writable.write(this.text) + writable.write(this.processFormatting(this.text)) + } + + processFormatting(text) { + return text.replace(new RegExp(ansi.ESC + '\\[0m', 'g'), + ansi.setAttributes([ansi.A_RESET, ...this.textAttributes])) } set text(newText) { diff --git a/ui/presentation/WrapLabel.js b/ui/presentation/WrapLabel.js index 0ecc777..eae8960 100644 --- a/ui/presentation/WrapLabel.js +++ b/ui/presentation/WrapLabel.js @@ -1,5 +1,3 @@ -import wrap from 'word-wrap' - import * as ansi from 'tui-lib/util/ansi' import Label from './Label.js' @@ -21,7 +19,7 @@ export default class WrapLabel extends Label { const lines = this.getWrappedLines() for (let i = 0; i < lines.length; i++) { writable.write(ansi.moveCursor(this.absTop + i, this.absLeft)) - writable.write(lines[i]) + writable.write(this.processFormatting(lines[i])) } } @@ -30,9 +28,7 @@ export default class WrapLabel extends Label { return [] } - const options = {width: this.w, indent: ''} - return wrap(this.text, options).split('\n') - .map(l => l.trim()) + return ansi.wrapToColumns(this.text, this.w - 1).map(l => l.trim()) } get h() { diff --git a/ui/primitives/DisplayElement.js b/ui/primitives/DisplayElement.js index d2a0956..6452887 100644 --- a/ui/primitives/DisplayElement.js +++ b/ui/primitives/DisplayElement.js @@ -237,7 +237,7 @@ export default class DisplayElement extends Element { children.reverse() for (const el of children) { - if (!el.visible || el.clickThrough) { + if (!el.visible) { continue } @@ -246,6 +246,10 @@ export default class DisplayElement extends Element { return el2 } + if (el.clickThrough) { + continue + } + const { absX, absY, w, h } = el if (absX <= x && absX + w > x) { if (absY <= y && absY + h > y) { diff --git a/ui/primitives/Element.js b/ui/primitives/Element.js index fea8c03..a5dbea6 100644 --- a/ui/primitives/Element.js +++ b/ui/primitives/Element.js @@ -1,5 +1,7 @@ import EventEmitter from 'node:events' +import exception from 'tui-lib/util/exception' + export default class Element extends EventEmitter { // The basic class containing methods for working with an element hierarchy. // Generally speaking, you usually want to extend DisplayElement instead of |