recalculate dynamic layout heights (#362)

* working on MagicLayout.  update constants and move to dynamic computation in textmeasure.

* magic line height -- no jitter.  and add some debugging code for helping to fix future problems

* fix openai fonts
This commit is contained in:
Mike Sawka 2024-02-29 23:46:22 -08:00 committed by GitHub
parent 41f91b6a8a
commit 402c8c2485
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 150 additions and 64 deletions

View File

@ -235,7 +235,7 @@
.cmdtext-expanded { .cmdtext-expanded {
overflow: auto; overflow: auto;
max-height: calc(var(--termlineheight) * 3.3); max-height: calc(var(--termlineheight) * 3);
white-space: pre; white-space: pre;
color: var(--term-bright-white); color: var(--term-bright-white);
font-weight: bold; font-weight: bold;

View File

@ -25,9 +25,31 @@ import * as lineutil from "./lineutil";
import { ErrorBoundary } from "@/common/error/errorboundary"; import { ErrorBoundary } from "@/common/error/errorboundary";
import * as appconst from "@/app/appconst"; import * as appconst from "@/app/appconst";
import * as util from "@/util/util"; import * as util from "@/util/util";
import * as textmeasure from "@/util/textmeasure";
import "./line.less"; import "./line.less";
const DebugHeightProblems = false;
const MinLine = 0;
const MaxLine = 1000;
let heightLog = {};
(window as any).heightLog = heightLog;
(window as any).findHeightProblems = function () {
for (let linenum in heightLog) {
let lh = heightLog[linenum];
if (lh.heightArr == null || lh.heightArr.length < 2) {
continue;
}
let firstHeight = lh.heightArr[0];
for (let i = 1; i < lh.heightArr.length; i++) {
if (lh.heightArr[i] != firstHeight) {
console.log("line", linenum, "heights", lh.heightArr);
break;
}
}
}
};
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
function cmdHasError(cmd: Cmd): boolean { function cmdHasError(cmd: Cmd): boolean {
@ -459,6 +481,12 @@ class LineCmd extends React.Component<
if (elem != null) { if (elem != null) {
curHeight = elem.offsetHeight; curHeight = elem.offsetHeight;
} }
let linenum = line.linenum;
if (DebugHeightProblems && linenum >= MinLine && linenum <= MaxLine) {
heightLog[linenum] = heightLog[linenum] || {};
heightLog[linenum].heightArr = heightLog[linenum].heightArr || [];
heightLog[linenum].heightArr.push(curHeight);
}
if (this.lastHeight == curHeight) { if (this.lastHeight == curHeight) {
return; return;
} }
@ -486,12 +514,11 @@ class LineCmd extends React.Component<
getTerminalRendererHeight(cmd: Cmd): number { getTerminalRendererHeight(cmd: Cmd): number {
const { screen, line, width } = this.props; const { screen, line, width } = this.props;
let height = 45 + 24; // height of zero height terminal
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width); const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
if (usedRows > 0) { if (usedRows == 0) {
height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.getTermFontSize(), cmd.getTermMaxRows()); return 0;
} }
return height; return termHeightFromRows(usedRows, GlobalModel.getTermFontSize(), cmd.getTermMaxRows());
} }
@boundMethod @boundMethod
@ -512,18 +539,18 @@ class LineCmd extends React.Component<
renderSimple() { renderSimple() {
const { screen, line } = this.props; const { screen, line } = this.props;
const cmd = screen.getCmd(line); const cmd = screen.getCmd(line);
let height: number = 0; let contentHeight: number = 0;
if (isBlank(line.renderer) || line.renderer == "terminal") { if (isBlank(line.renderer) || line.renderer == "terminal") {
height = this.getTerminalRendererHeight(cmd); contentHeight = this.getTerminalRendererHeight(cmd);
} else { } else {
// header is 16px tall with hide-prompt, 36px otherwise
const { screen, line, width } = this.props; const { screen, line, width } = this.props;
const hidePrompt = getIsHidePrompt(line); contentHeight = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width); }
height = (hidePrompt ? 16 + 6 : 36 + 6) + usedRows; const mainDivCn = cn("line", "line-cmd");
if (DebugHeightProblems && line.linenum >= MinLine && line.linenum <= MaxLine) {
heightLog[line.linenum] = heightLog[line.linenum] || {};
heightLog[line.linenum].contentHeight = contentHeight;
} }
const formattedTime = lineutil.getLineDateTimeStr(line.ts);
const mainDivCn = cn("line", "line-cmd", "line-simple");
return ( return (
<div <div
className={mainDivCn} className={mainDivCn}
@ -531,12 +558,12 @@ class LineCmd extends React.Component<
data-lineid={line.lineid} data-lineid={line.lineid}
data-linenum={line.linenum} data-linenum={line.linenum}
data-screenid={line.screenid} data-screenid={line.screenid}
style={{ height: height }}
> >
<div className="simple-line-header"> <LineHeader screen={screen} line={line} cmd={cmd} />
<SmallLineAvatar line={line} cmd={cmd} /> <div
<div className="ts">{formattedTime}</div> className={cn("line-content", { "zero-height": contentHeight == 0 })}
</div> style={{ height: contentHeight }}
/>
</div> </div>
); );
} }
@ -695,6 +722,12 @@ class LineCmd extends React.Component<
const termFontSize = GlobalModel.getTermFontSize(); const termFontSize = GlobalModel.getTermFontSize();
const containerType = screen.getContainerType(); const containerType = screen.getContainerType();
const isMinimized = line.linestate["wave:min"] && containerType == appconst.LineContainer_Main; const isMinimized = line.linestate["wave:min"] && containerType == appconst.LineContainer_Main;
const lhv: LineChromeHeightVars = {
numCmdLines: lineutil.countCmdLines(cmd.getCmdStr()),
zeroHeight: isMinimized,
hasLine2: !hidePrompt,
};
const chromeHeight = textmeasure.calcLineChromeHeight(GlobalModel.lineHeightEnv, lhv);
return ( return (
<div <div
className={mainDivCn} className={mainDivCn}

View File

@ -53,6 +53,18 @@ function isMultiLineCmdText(cmdText: string): boolean {
return nlIdx != -1; return nlIdx != -1;
} }
function countCmdLines(cmdText: string): number {
if (cmdText == null) {
return 1;
}
cmdText = cmdText.trim();
let nlIdx = cmdText.indexOf("\n");
if (nlIdx == -1) {
return 1;
}
return cmdText.split("\n").length;
}
function getFullCmdText(cmdText: string) { function getFullCmdText(cmdText: string) {
if (cmdText == null) { if (cmdText == null) {
return "(none)"; return "(none)";
@ -98,6 +110,7 @@ export {
getLineDateStr, getLineDateStr,
getLineDateTimeStr, getLineDateTimeStr,
isMultiLineCmdText, isMultiLineCmdText,
countCmdLines,
getFullCmdText, getFullCmdText,
getSingleLineCmdText, getSingleLineCmdText,
getRendererContext, getRendererContext,

View File

@ -4,17 +4,7 @@
// magical layout constants to power TypeScript calculations // magical layout constants to power TypeScript calculations
// these need to match the CSS (usually margins, paddings, positions, etc.) // these need to match the CSS (usually margins, paddings, positions, etc.)
let MagicLayout = { let MagicLayout = {
CmdInputHeight: 101, // height of full cmd-input div
CmdInputBottom: 12, // .cmd-input
LineHeaderHeight: 46, // .line-header
LinePadding: 24, // .line-header (12px * 2)
WindowHeightOffset: 6, // .window-view, height is calc(100%-0.5rem)
LinesBottomPadding: 10, // .lines, padding
LineMarginTop: 12, // .line, margin
ScreenMaxContentWidthBuffer: 50, ScreenMaxContentWidthBuffer: 50,
ScreenMaxContentHeightBuffer: 0, // calc below
ScreenMinContentSize: 100, ScreenMinContentSize: 100,
ScreenMaxContentSize: 5000, ScreenMaxContentSize: 5000,
@ -24,7 +14,7 @@ let MagicLayout = {
ScreenSidebarWidthPadding: 5, ScreenSidebarWidthPadding: 5,
ScreenSidebarMinWidth: 200, ScreenSidebarMinWidth: 200,
ScreenSidebarHeaderHeight: 28, ScreenSidebarHeaderHeight: 26,
MainSidebarMinWidth: 75, MainSidebarMinWidth: 75,
MainSidebarMaxWidth: 300, MainSidebarMaxWidth: 300,
@ -35,10 +25,6 @@ let MagicLayout = {
let m = MagicLayout; let m = MagicLayout;
// add up all the line overhead + padding. subtract 2 so we don't see the border of neighboring line
m.ScreenMaxContentHeightBuffer =
m.LineHeaderHeight + m.LinePadding + m.WindowHeightOffset + m.LinesBottomPadding + m.LineMarginTop - 2;
(window as any).MagicLayout = MagicLayout; (window as any).MagicLayout = MagicLayout;
export { MagicLayout }; export { MagicLayout };

View File

@ -23,6 +23,7 @@ import { ReactComponent as Check12Icon } from "@/assets/icons/check12.svg";
import { ReactComponent as GlobeIcon } from "@/assets/icons/globe.svg"; import { ReactComponent as GlobeIcon } from "@/assets/icons/globe.svg";
import { ReactComponent as StatusCircleIcon } from "@/assets/icons/statuscircle.svg"; import { ReactComponent as StatusCircleIcon } from "@/assets/icons/statuscircle.svg";
import * as appconst from "@/app/appconst"; import * as appconst from "@/app/appconst";
import * as textmeasure from "@/util/textmeasure";
import "./screenview.less"; import "./screenview.less";
import "./tabs.less"; import "./tabs.less";
@ -264,7 +265,7 @@ class ScreenSidebar extends React.Component<{ screen: Screen; width: string }, {
width: sidebarElem.offsetWidth, width: sidebarElem.offsetWidth,
height: height:
sidebarElem.offsetHeight - sidebarElem.offsetHeight -
MagicLayout.ScreenMaxContentHeightBuffer - textmeasure.calcMaxLineChromeHeight(GlobalModel.lineHeightEnv) -
MagicLayout.ScreenSidebarHeaderHeight, MagicLayout.ScreenSidebarHeaderHeight,
}; };
mobx.action(() => this.sidebarSize.set(size))(); mobx.action(() => this.sidebarSize.set(size))();

View File

@ -12,7 +12,7 @@ import { CmdInput } from "./cmdinput/cmdinput";
import { ScreenView } from "./screen/screenview"; import { ScreenView } from "./screen/screenview";
import { ScreenTabs } from "./screen/tabs"; import { ScreenTabs } from "./screen/tabs";
import { ErrorBoundary } from "@/common/error/errorboundary"; import { ErrorBoundary } from "@/common/error/errorboundary";
import { MagicLayout } from "../magiclayout"; import * as textmeasure from "@/util/textmeasure";
import "./workspace.less"; import "./workspace.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@ -34,7 +34,7 @@ class WorkspaceView extends React.Component<{}, {}> {
let activeScreen = session.getActiveScreen(); let activeScreen = session.getActiveScreen();
let cmdInputHeight = model.inputModel.cmdInputHeight.get(); let cmdInputHeight = model.inputModel.cmdInputHeight.get();
if (cmdInputHeight == 0) { if (cmdInputHeight == 0) {
cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools) cmdInputHeight = textmeasure.baseCmdInputHeight(GlobalModel.lineHeightEnv); // this is the base size of cmdInput (measured using devtools)
} }
let isHidden = GlobalModel.activeMainView.get() != "session"; let isHidden = GlobalModel.activeMainView.get() != "session";
let mainSidebarModel = GlobalModel.mainSidebarModel; let mainSidebarModel = GlobalModel.mainSidebarModel;

View File

@ -8,6 +8,7 @@ import { Model } from "./model";
import { GlobalCommandRunner } from "./global"; import { GlobalCommandRunner } from "./global";
import { Cmd } from "./cmd"; import { Cmd } from "./cmd";
import { Screen } from "./screen"; import { Screen } from "./screen";
import * as lineutil from "@/app/line/lineutil";
class ForwardLineContainer { class ForwardLineContainer {
globalModel: Model; globalModel: Model;
@ -30,7 +31,7 @@ class ForwardLineContainer {
if (termWrap != null) { if (termWrap != null) {
let fontSize = this.globalModel.getTermFontSize(); let fontSize = this.globalModel.getTermFontSize();
let cols = windowWidthToCols(winSize.width, fontSize); let cols = windowWidthToCols(winSize.width, fontSize);
let rows = windowHeightToRows(winSize.height, fontSize); let rows = windowHeightToRows(Model.getInstance().lineHeightEnv, this.winSize.height);
termWrap.resizeCols(cols); termWrap.resizeCols(cols);
GlobalCommandRunner.resizeScreen(this.screen.screenId, rows, cols, { include: [this.lineId] }); GlobalCommandRunner.resizeScreen(this.screen.screenId, rows, cols, { include: [this.lineId] });
} }

View File

@ -104,6 +104,7 @@ class Model {
name: "devicePixelRatio", name: "devicePixelRatio",
}); });
remotesModel: RemotesModel; remotesModel: RemotesModel;
lineHeightEnv: LineHeightEnv;
inputModel: InputModel; inputModel: InputModel;
pluginsModel: PluginsModel; pluginsModel: PluginsModel;
@ -343,27 +344,31 @@ class Model {
return this.termFontSize.get(); return this.termFontSize.get();
} }
updateTermFontSizeVars(fontSize: number, force: boolean) { updateTermFontSizeVars() {
if (!force && fontSize == this.termFontSize.get()) { let lhe = this.recomputeLineHeightEnv();
return; mobx.action(() => {
} this.bumpRenderVersion();
if (fontSize < appconst.MinFontSize) { this.setStyleVar("--termfontsize", lhe.fontSize + "px");
fontSize = appconst.MinFontSize; this.setStyleVar("--termlineheight", lhe.lineHeight + "px");
} this.setStyleVar("--termpad", lhe.pad + "px");
if (fontSize > appconst.MaxFontSize) { this.setStyleVar("--termfontsize-sm", lhe.fontSizeSm + "px");
fontSize = appconst.MaxFontSize; this.setStyleVar("--termlineheight-sm", lhe.lineHeightSm + "px");
} })();
}
recomputeLineHeightEnv(): LineHeightEnv {
const fontSize = this.getTermFontSize();
const fontSizeSm = fontSize - 2; const fontSizeSm = fontSize - 2;
const monoFontSize = getMonoFontSize(fontSize); const monoFontSize = getMonoFontSize(fontSize);
const monoFontSizeSm = getMonoFontSize(fontSizeSm); const monoFontSizeSm = getMonoFontSize(fontSizeSm);
mobx.action(() => { this.lineHeightEnv = {
this.bumpRenderVersion(); fontSize: fontSize,
this.setStyleVar("--termfontsize", fontSize + "px"); fontSizeSm: fontSizeSm,
this.setStyleVar("--termlineheight", monoFontSize.height + "px"); lineHeight: monoFontSize.height,
this.setStyleVar("--termpad", monoFontSize.pad + "px"); lineHeightSm: monoFontSizeSm.height,
this.setStyleVar("--termfontsize-sm", fontSizeSm + "px"); pad: monoFontSize.pad,
this.setStyleVar("--termlineheight-sm", monoFontSizeSm.height + "px"); };
})(); return this.lineHeightEnv;
} }
setStyleVar(name: string, value: string) { setStyleVar(name: string, value: string) {
@ -1181,11 +1186,11 @@ class Model {
loadFonts(newFontFamily); loadFonts(newFontFamily);
document.fonts.ready.then(() => { document.fonts.ready.then(() => {
clearMonoFontCache(); clearMonoFontCache();
this.updateTermFontSizeVars(this.termFontSize.get(), true); // forces an update of css vars this.updateTermFontSizeVars(); // forces an update of css vars
this.bumpRenderVersion(); this.bumpRenderVersion();
}); });
} else if (fsUpdated) { } else if (fsUpdated) {
this.updateTermFontSizeVars(newFontSize, true); this.updateTermFontSizeVars();
} }
} }

View File

@ -16,6 +16,7 @@ import { GlobalCommandRunner } from "./global";
import { Cmd } from "./cmd"; import { Cmd } from "./cmd";
import { ScreenLines } from "./screenlines"; import { ScreenLines } from "./screenlines";
import { getTermPtyData } from "@/util/modelutil"; import { getTermPtyData } from "@/util/modelutil";
import * as textmeasure from "@/util/textmeasure";
class Screen { class Screen {
globalModel: Model; globalModel: Model;
@ -402,8 +403,9 @@ class Screen {
return; return;
} }
this.lastScreenSize = winSize; this.lastScreenSize = winSize;
let useableHeight = winSize.height - textmeasure.calcMaxLineChromeHeight(this.globalModel.lineHeightEnv);
let cols = windowWidthToCols(winSize.width, this.globalModel.getTermFontSize()); let cols = windowWidthToCols(winSize.width, this.globalModel.getTermFontSize());
let rows = windowHeightToRows(winSize.height, this.globalModel.getTermFontSize()); let rows = windowHeightToRows(this.globalModel.lineHeightEnv, winSize.height);
this._termSizeCallback(rows, cols); this._termSizeCallback(rows, cols);
} }
@ -417,7 +419,8 @@ class Screen {
let minSize = MagicLayout.ScreenMinContentSize; let minSize = MagicLayout.ScreenMinContentSize;
let maxSize = MagicLayout.ScreenMaxContentSize; let maxSize = MagicLayout.ScreenMaxContentSize;
let width = boundInt(winSize.width - MagicLayout.ScreenMaxContentWidthBuffer, minSize, maxSize); let width = boundInt(winSize.width - MagicLayout.ScreenMaxContentWidthBuffer, minSize, maxSize);
let height = boundInt(winSize.height - MagicLayout.ScreenMaxContentHeightBuffer, minSize, maxSize); let maxLineBuffer = textmeasure.calcMaxLineChromeHeight(this.globalModel.lineHeightEnv);
let height = boundInt(winSize.height - maxLineBuffer, minSize, maxSize);
return { width, height }; return { width, height };
} }

View File

@ -1,4 +1,8 @@
.openai-renderer { .openai-renderer {
font-family: var(--termfontfamily);
font-size: var(--termfontsize);
line-height: var(--termlineheight);
.openai-message { .openai-message {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -19,11 +23,11 @@
.openai-content-user { .openai-content-user {
color: var(--app-text-color); color: var(--app-text-color);
font-family: var(--markdown-font); font-family: var(--markdown-font);
font-size: var(--markdown-font-size);
font-weight: normal; font-weight: normal;
} }
.openai-content-assistant { .openai-content-assistant {
font-family: var(--markdown-font);
color: var(--app-text-color); color: var(--app-text-color);
} }

View File

@ -305,7 +305,7 @@ class TermWrap {
resizeWindow(size: WindowSize): void { resizeWindow(size: WindowSize): void {
let cols = windowWidthToCols(size.width, this.fontSize); let cols = windowWidthToCols(size.width, this.fontSize);
let rows = windowHeightToRows(size.height, this.fontSize); let rows = windowHeightToRows(GlobalModel.lineHeightEnv, size.height);
this.resize({ rows, cols }); this.resize({ rows, cols });
} }

16
src/types/custom.d.ts vendored
View File

@ -845,6 +845,22 @@ declare global {
getContainerType(): LineContainerStrs; getContainerType(): LineContainerStrs;
}; };
// the "environment" for computing a line's height (stays constant for a given term font family / size)
type LineHeightEnv = {
fontSize: number;
fontSizeSm: number;
lineHeight: number;
lineHeightSm: number;
pad: number;
};
// the "variables" for computing a line's height (changes per line)
type LineChromeHeightVars = {
numCmdLines: number;
zeroHeight: boolean;
hasLine2: boolean;
};
type MonoFontSize = { type MonoFontSize = {
height: number; height: number;
width: number; width: number;

View File

@ -71,9 +71,8 @@ function windowWidthToCols(width: number, fontSize: number): number {
return cols; return cols;
} }
function windowHeightToRows(height: number, fontSize: number): number { function windowHeightToRows(lhe: LineHeightEnv, height: number): number {
let dr = getMonoFontSize(fontSize); let rows = Math.floor((height - calcMaxLineChromeHeight(lhe)) / lhe.lineHeight) - 1;
let rows = Math.floor((height - MagicLayout.ScreenMaxContentHeightBuffer) / dr.height) - 1;
if (rows <= 0) { if (rows <= 0) {
rows = 1; rows = 1;
} }
@ -99,6 +98,28 @@ function termHeightFromRows(rows: number, fontSize: number, totalRows: number):
return Math.ceil(realHeight * rows); return Math.ceil(realHeight * rows);
} }
function calcLineChromeHeight(lhe: LineHeightEnv, lhv: LineChromeHeightVars): number {
const topPadding = lhe.pad * 2;
const botPadding = lhe.pad * 2 + 1;
const headerLine1 = lhe.lineHeightSm;
const headerLine2 = lhv.hasLine2 ? lhe.lineHeight * Math.min(lhv.numCmdLines, 3) + 2 : 0;
const contentSpacer = lhv.zeroHeight ? 0 : lhe.pad + 2;
return topPadding + botPadding + headerLine1 + headerLine2 + contentSpacer;
}
function calcMaxLineChromeHeight(lhe: LineHeightEnv): number {
return calcLineChromeHeight(lhe, { numCmdLines: 3, hasLine2: true, zeroHeight: false });
}
function baseCmdInputHeight(lhe: LineHeightEnv): number {
const topPadding = lhe.pad * 2;
const botPadding = lhe.pad * 2;
const border = 2;
const cmdInputContext = lhe.lineHeight;
const textArea = lhe.lineHeight + lhe.pad * 2 + lhe.pad * 2; // lineHeight + innerPad + outerPad
return topPadding + botPadding + border + cmdInputContext + textArea;
}
export { export {
measureText, measureText,
getMonoFontSize, getMonoFontSize,
@ -108,4 +129,7 @@ export {
termHeightFromRows, termHeightFromRows,
clearMonoFontCache, clearMonoFontCache,
MonoFontSizes, MonoFontSizes,
calcLineChromeHeight,
calcMaxLineChromeHeight,
baseCmdInputHeight,
}; };