From 7177a495cf67bbf4e6a0d6cef424f0a0b8aa07cd Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 22 Dec 2022 11:06:30 +0700 Subject: [PATCH] feat(web): variable-width suggestions, padding-fill if total width too narrow --- .../engine/osk/src/banner/suggestionBanner.ts | 165 +++++++++++++++--- 1 file changed, 139 insertions(+), 26 deletions(-) diff --git a/web/src/engine/osk/src/banner/suggestionBanner.ts b/web/src/engine/osk/src/banner/suggestionBanner.ts index fd2530735e..89004b584d 100644 --- a/web/src/engine/osk/src/banner/suggestionBanner.ts +++ b/web/src/engine/osk/src/banner/suggestionBanner.ts @@ -13,10 +13,25 @@ import { ParsedLengthStyle } from '../lengthStyle.js'; import { getFontSizeStyle } from '../fontSizeUtils.js'; import { getTextMetrics } from '../keyboard-layout/getTextMetrics.js'; +// TODO: finalize + document +interface OptionFormatSpec { + minWidth?: number; + paddingWidth: number, + emSize: number, + styleForFont: CSSStyleDeclaration + + collapsedWidth?: number +} export class BannerSuggestion { div: HTMLDivElement; container: HTMLDivElement; private display: HTMLSpanElement; + + private _collapsedWidth: number; + private _textWidth: number; + private _minWidth: number; + private _paddingWidth: number; + private fontFamily?: string; private rtl: boolean = false; @@ -90,41 +105,95 @@ export class BannerSuggestion { * @param collapsedTargetWidth * Description Update the ID and text of the BannerSuggestionSpec */ - public update( - suggestion: Suggestion, - fontStyle: CSSStyleDeclaration, - emSize: number, - targetWidth: number, - collapsedTargetWidth: number - ) { + public update(suggestion: Suggestion, format: OptionFormatSpec) { this._suggestion = suggestion; - // TODO: if the option is highlighted, maybe don't disable transitions? - this.container.style.transition = 'none'; // temporarily disable transition effects. - let display = this.generateSuggestionText(this.rtl); this.container.replaceChild(display, this.display); this.display = display; - // Compute the raw text-width of the suggestion and determine specs for the default (collapsed) styling. - const optionCollapseStyle = this.container.style; - - const rawMetrics = getTextMetrics(suggestion.displayAs, emSize, fontStyle); - const rawTextWidth = rawMetrics.width; - optionCollapseStyle.minWidth = `${targetWidth}px`; - - if(rawTextWidth > collapsedTargetWidth) { - optionCollapseStyle.marginLeft = `${collapsedTargetWidth - rawTextWidth}px`; - } else { - optionCollapseStyle.marginLeft = '0px'; + // Set internal properties for use in format calculations. + if(format.minWidth !== undefined) { + this._minWidth = format.minWidth; } + this._paddingWidth = format.paddingWidth; + this._collapsedWidth = format.collapsedWidth; + + if(suggestion && suggestion.displayAs) { + const rawMetrics = getTextMetrics(suggestion.displayAs, format.emSize, format.styleForFont); + this._textWidth = rawMetrics.width; + } else { + this._textWidth = 0; + } + + this.updateLayout(); + } + + public updateLayout() { + if(!this.suggestion && this.index != 0) { + this.div.style.width='0px'; + return; + } else { + this.div.style.width=''; + } + + // TODO: if the option is highlighted, maybe don't disable transitions? + this.container.style.transition = 'none'; // temporarily disable transition effects. + + const collapserStyle = this.container.style; + collapserStyle.minWidth = this.collapsedWidth + 'px'; + collapserStyle.marginLeft = (this.collapsedWidth - this.expandedWidth) + 'px'; + this.container.offsetWidth; // To 'flush' the changes before re-enabling transition animations. this.container.offsetLeft; this.container.style.transition = ''; // Re-enable them (it's set on the element's class) } + + + public get targetCollapsedWidth(): number { + return this._collapsedWidth; + } + + public get textWidth(): number { + return this._textWidth; + } + + public get paddingWidth(): number { + return this._paddingWidth; + } + + public get minWidth(): number { + return this._minWidth; + } + + public set minWidth(val: number) { + this._minWidth = val; + } + + public get expandedWidth(): number { + // minWidth must be defined AND greater for the conditional to return this.minWidth. + return this.minWidth > this.spanWidth ? this.minWidth : this.spanWidth; + } + + public get spanWidth(): number { + let spanWidth = this.textWidth ?? 0; + if(spanWidth) { + spanWidth += this.paddingWidth ?? 0; + } + + return spanWidth; + } + + public get collapsedWidth(): number { + let maxWidth = this.targetCollapsedWidth < this.expandedWidth ? this.targetCollapsedWidth : this.expandedWidth; + + // Will return maxWidth if this.minWidth is undefined. + return (this.minWidth > maxWidth ? this.minWidth : maxWidth); + } + public isEmpty(): boolean { return !this._suggestion; } @@ -177,6 +246,8 @@ export class SuggestionBanner extends Banner { private currentSuggestions: Suggestion[] = []; private options : BannerSuggestion[] = []; + private separators: HTMLElement[] = []; + private hostDevice: DeviceSpec; private manager: SuggestionInputManager; @@ -211,8 +282,10 @@ export class SuggestionBanner extends Banner { buildInternals(rtl: boolean) { if(this.options.length > 0) { - this.options.splice(0, this.options.length); // Clear the array. + this.options = []; + this.separators = []; } + for (var i=0; i { - option.update(i < suggestions.length ? suggestions[i] : null, fontStyle, emSize, targetWidth, collapsedTargetWidth); - }); + let totalWidth = 0; + let displayCount = 0; + + for (let i=0; i i) { + const suggestion = suggestions[i]; + d.update(suggestion, optionFormat); + + totalWidth += d.collapsedWidth; + displayCount++; + } else { + d.update(null, optionFormat); + } + } + + // Ensure one suggestion is always displayed, even if empty. (Keep the separators out) + displayCount = displayCount || 1; + + if(totalWidth < this.width) { + let separatorWidth = (this.width * 0.01 * (displayCount-1)); + let fillPadding = (this.width - totalWidth - separatorWidth) / displayCount; + + for(let i=0; i < displayCount; i++) { + const d = this.options[i]; + + d.minWidth = d.collapsedWidth + fillPadding; + d.updateLayout(); + } + } + + // Hide any separators beyond the final displayed suggestion + for(let i=0; i < SuggestionBanner.SUGGESTION_LIMIT - 1; i++) { + this.separators[i].style.display = i < displayCount - 1 ? '' : 'none'; + } } }