diff --git a/src/app/appconst.ts b/src/app/appconst.ts index 742331801..d8b05e742 100644 --- a/src/app/appconst.ts +++ b/src/app/appconst.ts @@ -18,7 +18,8 @@ export const ConfirmKey_HideShellPrompt = "hideshellprompt"; export const NoStrPos = -1; -export const RemotePtyRows = 8; // also in main.tsx +export const RemotePtyRows = 8; +export const RemotePtyTotalRows = 25; export const RemotePtyCols = 80; export const ProdServerEndpoint = "http://127.0.0.1:1619"; export const ProdServerWsEndpoint = "ws://127.0.0.1:1623"; diff --git a/src/app/common/modals/viewremoteconndetail.tsx b/src/app/common/modals/viewremoteconndetail.tsx index 48536c603..cc93e122c 100644 --- a/src/app/common/modals/viewremoteconndetail.tsx +++ b/src/app/common/modals/viewremoteconndetail.tsx @@ -370,7 +370,11 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> { ref={this.termRef} data-remoteid={remote.remoteid} style={{ - height: textmeasure.termHeightFromRows(appconst.RemotePtyRows, termFontSize), + height: textmeasure.termHeightFromRows( + appconst.RemotePtyRows, + termFontSize, + appconst.RemotePtyTotalRows + ), width: termWidth, }} > diff --git a/src/app/line/linecomps.tsx b/src/app/line/linecomps.tsx index 306b03588..1b75962b6 100644 --- a/src/app/line/linecomps.tsx +++ b/src/app/line/linecomps.tsx @@ -388,7 +388,7 @@ class LineCmd extends React.Component< let height = 45 + 24; // height of zero height terminal const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width); if (usedRows > 0) { - height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get()); + height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get(), cmd.getTermMaxRows()); } return height; } diff --git a/src/app/line/lines.less b/src/app/line/lines.less index 19c5bc62a..6291d49e7 100644 --- a/src/app/line/lines.less +++ b/src/app/line/lines.less @@ -464,6 +464,7 @@ padding: 0 0 10px 0; flex-grow: 1; position: relative; + overflow-x: hidden; &::-webkit-scrollbar-thumb { background-color: transparent !important; diff --git a/src/app/magiclayout.ts b/src/app/magiclayout.ts index 4b2bb70f4..76d73dc92 100644 --- a/src/app/magiclayout.ts +++ b/src/app/magiclayout.ts @@ -18,8 +18,6 @@ let MagicLayout = { ScreenMinContentSize: 100, ScreenMaxContentSize: 5000, - // the 3 is for descenders, which get cut off in the terminal without this - TermDescendersHeight: 3, TermWidthBuffer: 15, TabWidth: 154, diff --git a/src/app/workspace/screen/screenview.tsx b/src/app/workspace/screen/screenview.tsx index 9e88fb2f2..f37dce701 100644 --- a/src/app/workspace/screen/screenview.tsx +++ b/src/app/workspace/screen/screenview.tsx @@ -109,6 +109,7 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, { return
; } let fontSize = GlobalModel.termFontSize.get(); + let dprStr = sprintf("%0.3f", GlobalModel.devicePixelRatio.get()); let viewOpts = screen.viewOpts.get(); let hasSidebar = viewOpts?.sidebar?.open; let winWidth = "100%"; @@ -145,7 +146,7 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, { return (
{ + if (MainWindow == null) { + return; + } + MainWindow.webContents.setZoomFactor(1); + MainWindow.webContents.send("zoom-changed"); + }, + }, + { + label: "Zoom In", + accelerator: cmdOrAlt + "+Plus", + click: () => { + if (MainWindow == null) { + return; + } + const zoomFactor = MainWindow.webContents.getZoomFactor(); + MainWindow.webContents.setZoomFactor(zoomFactor * 1.1); + MainWindow.webContents.send("zoom-changed"); + }, + }, + { + label: "Zoom Out", + accelerator: cmdOrAlt + "+-", + click: () => { + if (MainWindow == null) { + return; + } + const zoomFactor = MainWindow.webContents.getZoomFactor(); + MainWindow.webContents.setZoomFactor(zoomFactor / 1.1); + MainWindow.webContents.send("zoom-changed"); + }, + }, { type: "separator" }, { role: "togglefullscreen" }, ], @@ -375,6 +408,9 @@ function createMainWindow(clientData: ClientDataType | null) { win.on("close", () => { MainWindow = null; }); + win.webContents.on("zoom-changed", (e) => { + win.webContents.send("zoom-changed"); + }); win.webContents.setWindowOpenHandler(({ url, frameName }) => { if (url.startsWith("https://docs.waveterm.dev/")) { console.log("openExternal docs", url); diff --git a/src/electron/preload.js b/src/electron/preload.js index e7be2ec57..cd807c652 100644 --- a/src/electron/preload.js +++ b/src/electron/preload.js @@ -21,6 +21,7 @@ contextBridge.exposeInMainWorld("api", { onWCmd: (callback) => ipcRenderer.on("w-cmd", callback), onPCmd: (callback) => ipcRenderer.on("p-cmd", callback), onRCmd: (callback) => ipcRenderer.on("r-cmd", callback), + onZoomChanged: (callback) => ipcRenderer.on("zoom-changed", callback), onMetaArrowUp: (callback) => ipcRenderer.on("meta-arrowup", callback), onMetaArrowDown: (callback) => ipcRenderer.on("meta-arrowdown", callback), onMetaPageUp: (callback) => ipcRenderer.on("meta-pageup", callback), diff --git a/src/index.ts b/src/index.ts index 87ebfcf2c..2a2c699b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { sprintf } from "sprintf-js"; import { App } from "@/app/app"; import * as DOMPurify from "dompurify"; import { loadFonts } from "./util/util"; +import * as textmeasure from "./util/textmeasure"; // @ts-ignore let VERSION = __WAVETERM_VERSION__; @@ -28,5 +29,6 @@ document.addEventListener("DOMContentLoaded", () => { (window as any).mobx = mobx; (window as any).sprintf = sprintf; (window as any).DOMPurify = DOMPurify; +(window as any).textmeasure = textmeasure; console.log("WaveTerm", VERSION, BUILD); diff --git a/src/models/cmd.ts b/src/models/cmd.ts index a55eb0749..d2ab3aa85 100644 --- a/src/models/cmd.ts +++ b/src/models/cmd.ts @@ -88,6 +88,11 @@ class Cmd { return this.data.get().termopts; } + getTermMaxRows(): number { + let termOpts = this.getTermOpts(); + return termOpts?.rows; + } + getCmdStr(): string { return this.data.get().cmdstr; } diff --git a/src/models/historyview.ts b/src/models/historyview.ts index 44c0649a0..b309ac9ed 100644 --- a/src/models/historyview.ts +++ b/src/models/historyview.ts @@ -115,7 +115,7 @@ class HistoryViewModel { } else { this.activeItem.set(hitem.historyid); let width = termWidthFromCols(80, this.globalModel.termFontSize.get()); - let height = termHeightFromRows(25, this.globalModel.termFontSize.get()); + let height = termHeightFromRows(25, this.globalModel.termFontSize.get(), 25); this.specialLineContainer = new SpecialLineContainer( this, { width, height }, @@ -149,7 +149,7 @@ class HistoryViewModel { } _deleteSelected(): void { - let lineIds = Array.from(this.selectedItems.keys()); + let lineIds: string[] = Array.from(this.selectedItems.keys()); let prtn = GlobalCommandRunner.historyPurgeLines(lineIds); prtn.then((result: CommandRtnType) => { if (!result.success) { diff --git a/src/models/model.ts b/src/models/model.ts index d3f5841d9..f2f9dc980 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -32,13 +32,8 @@ import { MainSidebarModel } from "./mainsidebar"; import { Screen } from "./screen"; import { Cmd } from "./cmd"; import { GlobalCommandRunner } from "./global"; - -type KeyModsType = { - meta?: boolean; - ctrl?: boolean; - alt?: boolean; - shift?: boolean; -}; +import { clearMonoFontCache } from "@/util/textmeasure"; +import type { TermWrap } from "@/plugins/terminal/term"; type SWLinePtr = { line: LineType; @@ -46,36 +41,6 @@ type SWLinePtr = { screen: Screen; }; -type ElectronApi = { - getId: () => string; - getIsDev: () => boolean; - getPlatform: () => string; - getAuthKey: () => string; - getWaveSrvStatus: () => boolean; - restartWaveSrv: () => boolean; - reloadWindow: () => void; - openExternalLink: (url: string) => void; - reregisterGlobalShortcut: (shortcut: string) => void; - onTCmd: (callback: (mods: KeyModsType) => void) => void; - onICmd: (callback: (mods: KeyModsType) => void) => void; - onLCmd: (callback: (mods: KeyModsType) => void) => void; - onHCmd: (callback: (mods: KeyModsType) => void) => void; - onPCmd: (callback: (mods: KeyModsType) => void) => void; - onRCmd: (callback: (mods: KeyModsType) => void) => void; - onWCmd: (callback: (mods: KeyModsType) => void) => void; - onMenuItemAbout: (callback: () => void) => void; - onMetaArrowUp: (callback: () => void) => void; - onMetaArrowDown: (callback: () => void) => void; - onMetaPageUp: (callback: () => void) => void; - onMetaPageDown: (callback: () => void) => void; - onBracketCmd: (callback: (event: any, arg: { relative: number }, mods: KeyModsType) => void) => void; - onDigitCmd: (callback: (event: any, arg: { digit: number }, mods: KeyModsType) => void) => void; - contextScreen: (screenOpts: { screenId: string }, position: { x: number; y: number }) => void; - contextEditMenu: (position: { x: number; y: number }, opts: ContextMenuOpts) => void; - onWaveSrvStatusChange: (callback: (status: boolean, pid: number) => void) => void; - getLastLogs: (numOfLines: number, callback: (logs: any) => void) => void; -}; - function getApi(): ElectronApi { return (window as any).api; } @@ -133,7 +98,10 @@ class Model { }); lineSettingsModal: OV = mobx.observable.box(null, { name: "lineSettingsModal", - }); // linenum + }); + devicePixelRatio: OV = mobx.observable.box(window.devicePixelRatio, { + name: "devicePixelRatio", + }); remotesModel: RemotesModel; inputModel: InputModel; @@ -196,6 +164,7 @@ class Model { getApi().onPCmd(this.onPCmd.bind(this)); getApi().onWCmd(this.onWCmd.bind(this)); getApi().onRCmd(this.onRCmd.bind(this)); + getApi().onZoomChanged(this.onZoomChanged.bind(this)); getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this)); getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this)); getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this)); @@ -514,6 +483,30 @@ class Model { } } + onZoomChanged(): void { + mobx.action(() => { + this.devicePixelRatio.set(window.devicePixelRatio); + clearMonoFontCache(); + })(); + } + + // for debuggin + getSelectedTermWrap(): TermWrap { + let screen = this.getActiveScreen(); + if (screen == null) { + return null; + } + let lineNum = screen.selectedLine.get(); + if (lineNum == null) { + return null; + } + let line = screen.getLineByNum(lineNum); + if (line == null) { + return null; + } + return screen.getTermWrap(line.lineid); + } + clearModals(): boolean { let didSomething = false; mobx.action(() => { diff --git a/src/models/model_old.ts-deprecated b/src/models/model_old.ts-deprecated index 5368a8937..d0cc731bb 100644 --- a/src/models/model_old.ts-deprecated +++ b/src/models/model_old.ts-deprecated @@ -205,6 +205,7 @@ type ElectronApi = { onPCmd: (callback: (mods: KeyModsType) => void) => void; onRCmd: (callback: (mods: KeyModsType) => void) => void; onWCmd: (callback: (mods: KeyModsType) => void) => void; + onZoomChanged: (callback: () => void) => void; onMenuItemAbout: (callback: () => void) => void; onMetaArrowUp: (callback: () => void) => void; onMetaArrowDown: (callback: () => void) => void; @@ -308,6 +309,11 @@ class Cmd { return this.data.get().termopts; } + getTermMaxRows(): number { + let termOpts = this.getTermOpts(); + return termOpts?.rows; + } + getCmdStr(): string { return this.data.get().cmdstr; } @@ -743,7 +749,7 @@ class Screen { getMaxContentSize(): WindowSize { if (this.lastScreenSize == null) { let width = termWidthFromCols(80, GlobalModel.termFontSize.get()); - let height = termHeightFromRows(25, GlobalModel.termFontSize.get()); + let height = termHeightFromRows(25, GlobalModel.termFontSize.get(), 25); return { width, height }; } let winSize = this.lastScreenSize; @@ -757,7 +763,7 @@ class Screen { getIdealContentSize(): WindowSize { if (this.lastScreenSize == null) { let width = termWidthFromCols(80, GlobalModel.termFontSize.get()); - let height = termHeightFromRows(25, GlobalModel.termFontSize.get()); + let height = termHeightFromRows(25, GlobalModel.termFontSize.get(), 25); return { width, height }; } let winSize = this.lastScreenSize; @@ -2396,7 +2402,7 @@ class HistoryViewModel { } else { this.activeItem.set(hitem.historyid); let width = termWidthFromCols(80, GlobalModel.termFontSize.get()); - let height = termHeightFromRows(25, GlobalModel.termFontSize.get()); + let height = termHeightFromRows(25, GlobalModel.termFontSize.get(), 25); this.specialLineContainer = new SpecialLineContainer( this, { width, height }, @@ -3451,6 +3457,9 @@ class Model { lineSettingsModal: OV = mobx.observable.box(null, { name: "lineSettingsModal", }); // linenum + devicePixelRatio: OV = mobx.observable.box(window.devicePixelRatio, { + name: "devicePixelRatio", + }); remotesModalModel: RemotesModalModel; remotesModel: RemotesModel; @@ -3515,6 +3524,7 @@ class Model { getApi().onPCmd(this.onPCmd.bind(this)); getApi().onWCmd(this.onWCmd.bind(this)); getApi().onRCmd(this.onRCmd.bind(this)); + getApi().onZoomChanged(this.onZoomChanged.bind(this)); getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this)); getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this)); getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this)); @@ -3826,6 +3836,29 @@ class Model { } } + onZoomChanged(): void { + mobx.action(() => { + this.devicePixelRatio.set(window.devicePixelRatio); + clearMonoFontCache(); + })(); + } + + getSelectedTermWrap(): TermWrap { + let screen = this.getActiveScreen(); + if (screen == null) { + return null; + } + let lineNum = screen.selectedLine.get(); + if (lineNum == null) { + return null; + } + let line = screen.getLineByNum(lineNum); + if (line == null) { + return null; + } + return screen.getTermWrap(line.lineid); + } + clearModals(): boolean { let didSomething = false; mobx.action(() => { diff --git a/src/models/screen.ts b/src/models/screen.ts index 9b0366711..624554711 100644 --- a/src/models/screen.ts +++ b/src/models/screen.ts @@ -410,7 +410,7 @@ class Screen { getMaxContentSize(): WindowSize { if (this.lastScreenSize == null) { let width = termWidthFromCols(80, this.globalModel.termFontSize.get()); - let height = termHeightFromRows(25, this.globalModel.termFontSize.get()); + let height = termHeightFromRows(25, this.globalModel.termFontSize.get(), 25); return { width, height }; } let winSize = this.lastScreenSize; @@ -424,7 +424,7 @@ class Screen { getIdealContentSize(): WindowSize { if (this.lastScreenSize == null) { let width = termWidthFromCols(80, this.globalModel.termFontSize.get()); - let height = termHeightFromRows(25, this.globalModel.termFontSize.get()); + let height = termHeightFromRows(25, this.globalModel.termFontSize.get(), 25); return { width, height }; } let winSize = this.lastScreenSize; diff --git a/src/plugins/terminal/terminal.tsx b/src/plugins/terminal/terminal.tsx index 261056265..31bd33f1e 100644 --- a/src/plugins/terminal/terminal.tsx +++ b/src/plugins/terminal/terminal.tsx @@ -148,7 +148,7 @@ class TerminalRenderer extends React.Component< .get(); let cmd = screen.getCmd(line); // will not be null let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width); - let termHeight = termHeightFromRows(usedRows, GlobalModel.termFontSize.get()); + let termHeight = termHeightFromRows(usedRows, GlobalModel.termFontSize.get(), cmd.getTermMaxRows()); if (usedRows === 0) { termHeight = 0; } @@ -164,6 +164,7 @@ class TerminalRenderer extends React.Component< { "zero-height": termHeight == 0 }, { collapsed: collapsed } )} + data-usedrows={usedRows} >
diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts index 500746c2a..f93f76423 100644 --- a/src/types/custom.d.ts +++ b/src/types/custom.d.ts @@ -833,6 +833,44 @@ declare global { isLineIdInSidebar(lineId: string): boolean; getContainerType(): LineContainerStrs; }; + + type KeyModsType = { + meta?: boolean; + ctrl?: boolean; + alt?: boolean; + shift?: boolean; + }; + + type ElectronApi = { + getId: () => string; + getIsDev: () => boolean; + getPlatform: () => string; + getAuthKey: () => string; + getWaveSrvStatus: () => boolean; + restartWaveSrv: () => boolean; + reloadWindow: () => void; + openExternalLink: (url: string) => void; + reregisterGlobalShortcut: (shortcut: string) => void; + onTCmd: (callback: (mods: KeyModsType) => void) => void; + onICmd: (callback: (mods: KeyModsType) => void) => void; + onLCmd: (callback: (mods: KeyModsType) => void) => void; + onHCmd: (callback: (mods: KeyModsType) => void) => void; + onPCmd: (callback: (mods: KeyModsType) => void) => void; + onRCmd: (callback: (mods: KeyModsType) => void) => void; + onWCmd: (callback: (mods: KeyModsType) => void) => void; + onZoomChanged: (callback: () => void) => void; + onMenuItemAbout: (callback: () => void) => void; + onMetaArrowUp: (callback: () => void) => void; + onMetaArrowDown: (callback: () => void) => void; + onMetaPageUp: (callback: () => void) => void; + onMetaPageDown: (callback: () => void) => void; + onBracketCmd: (callback: (event: any, arg: { relative: number }, mods: KeyModsType) => void) => void; + onDigitCmd: (callback: (event: any, arg: { digit: number }, mods: KeyModsType) => void) => void; + contextScreen: (screenOpts: { screenId: string }, position: { x: number; y: number }) => void; + contextEditMenu: (position: { x: number; y: number }, opts: ContextMenuOpts) => void; + onWaveSrvStatusChange: (callback: (status: boolean, pid: number) => void) => void; + getLastLogs: (numOfLines: number, callback: (logs: any) => void) => void; + }; } export {}; diff --git a/src/util/textmeasure.ts b/src/util/textmeasure.ts index 9551e00bd..5f3f5ca91 100644 --- a/src/util/textmeasure.ts +++ b/src/util/textmeasure.ts @@ -30,6 +30,10 @@ function getMonoFontSize(fontSize: number): { height: number; width: number } { return size; } +function clearMonoFontCache(): void { + MonoFontSizes = []; +} + function measureText( text: string, textOpts?: { pre?: boolean; mono?: boolean; fontSize?: number | string } @@ -57,8 +61,9 @@ function measureText( throw new Error("cannot measure text, no #measure div"); } measureDiv.replaceChildren(textElem); - let rect = textElem.getBoundingClientRect(); - return { width: rect.width, height: Math.ceil(rect.height) }; + let height = textElem.offsetHeight; + let width = textElem.offsetWidth; + return { width: width, height: Math.ceil(height) }; } function windowWidthToCols(width: number, fontSize: number): number { @@ -82,10 +87,27 @@ function termWidthFromCols(cols: number, fontSize: number): number { return Math.ceil(dr.width * cols) + MagicLayout.TermWidthBuffer; } -function termHeightFromRows(rows: number, fontSize: number): number { +// we need to match the xtermjs calculation in CharSizeService.ts and DomRenderer.ts +// it does some crazy rounding depending on the value of window.devicePixelRatio +// works out to `realHeight = round(ceil(height * dpr) * rows / dpr) / rows` +// their calculation is based off the "totalRows" (so that argument has been added) +function termHeightFromRows(rows: number, fontSize: number, totalRows: number): number { let dr = getMonoFontSize(fontSize); - // TODO: replace the TermDescendersHeight with some calculation based on termFontSize. - return Math.ceil(dr.height * rows) + MagicLayout.TermDescendersHeight; + const dpr = window.devicePixelRatio; + if (totalRows == null || totalRows == 0) { + totalRows = rows > 25 ? rows : 25; + } + let realHeight = Math.round((Math.ceil(dr.height * dpr) * totalRows) / dpr) / totalRows; + return Math.ceil(realHeight * rows); } -export { measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows }; +export { + measureText, + getMonoFontSize, + windowWidthToCols, + windowHeightToRows, + termWidthFromCols, + termHeightFromRows, + clearMonoFontCache, + MonoFontSizes, +};