From 62b9503387395bb3a60578646e22229fb9691608 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 6 Feb 2023 00:31:34 -0800 Subject: [PATCH] sort working image renderer --- src/imagerenderer.ts | 66 ++++++++++++++++++++++-- src/main.tsx | 118 ++++++++++++++++++++++++++++++++++++++++++- src/model.ts | 10 +++- src/sh2.less | 11 ++++ src/types.ts | 1 + src/util.ts | 8 ++- 6 files changed, 206 insertions(+), 8 deletions(-) diff --git a/src/imagerenderer.ts b/src/imagerenderer.ts index eab7214c..990a040d 100644 --- a/src/imagerenderer.ts +++ b/src/imagerenderer.ts @@ -1,18 +1,32 @@ import * as mobx from "mobx"; -import {WindowSize} from "./types"; +import {WindowSize, RendererContext, TermOptsType} from "./types"; +import {getPtyData, termWidthFromCols, termHeightFromRows} from "./model"; +import {incObs} from "./util"; const InitialSize = 10*1024; const IncreaseFactor = 1.5; -class ImageRenderer { +class ImageRendererModel { + context : RendererContext; dataSize : number; imageData : Uint8Array; brokenData : boolean; isDone : mobx.IObservableValue; + reloading : boolean = false; + dataVersion : mobx.IObservableValue; + htmlImgDivElem : any; + htmlImg : any; + termOpts : TermOptsType; - constructor() { + constructor(imgDivElem : any, context : RendererContext, termOpts : TermOptsType, isDone : boolean) { + this.htmlImgDivElem = imgDivElem; + this.termOpts = termOpts; + this.context = context; this._resetData(); - this.isDone = mobx.observable.box(false, {name: "isDone"}); + this.isDone = mobx.observable.box(isDone, {name: "isDone"}); + this.dataVersion = mobx.observable.box(0, {name: "dataVersion"}); + this.reload(0); + console.log("image", this.termOpts); } _resetData() { @@ -23,9 +37,44 @@ class ImageRenderer { dispose() : void { this._resetData(); + this.removeImage(); + } + + removeImage() : void { + this.htmlImg = null; + this.htmlImgDivElem.replaceChildren(); + } + + renderImage() : void { + if (!this.isDone.get()) { + return; + } + let blob = new Blob([this.imageData.slice(0, this.dataSize)], {type: "image/jpeg"}); + this.htmlImg = new Image(); + this.htmlImg.src = URL.createObjectURL(blob); + this.htmlImg.style.maxHeight = termHeightFromRows(this.termOpts.rows) + "px"; + this.htmlImg.style.maxWidth = termWidthFromCols(this.termOpts.cols) + "px"; + this.htmlImgDivElem.replaceChildren(this.htmlImg); } reload(delayMs : number) : void { + if (this.reloading) { + return; + } + this._resetData(); + this.reloading = true; + let rtnp = getPtyData(this.context.sessionId, this.context.cmdId); + rtnp.then((ptydata) => { + setTimeout(() => { + this.reloading = false; + this.receiveData(ptydata.pos, ptydata.data, "reload"); + this.renderImage(); + }, delayMs); + }).catch((e) => { + this.brokenData = true; + this.reloading = false; + console.log("error reloading image data", e); + }); } _growArray(minSize : number) : void { @@ -48,18 +97,25 @@ class ImageRenderer { } this.imageData.set(data, pos); this.dataSize += data.length; + incObs(this.dataVersion); } cmdDone() : void { + mobx.action(() => { + this.isDone.set(true); + })(); } resizeWindow(size : WindowSize) : void { + return; } resizeCols(cols : number) : void { + return; } giveFocus() : void { + return; } getUsedRows() : number { @@ -67,5 +123,5 @@ class ImageRenderer { } } -export {ImageRenderer}; +export {ImageRendererModel}; diff --git a/src/main.tsx b/src/main.tsx index 937069ce..1b33583b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -13,6 +13,7 @@ import type {SessionDataType, LineType, CmdDataType, RemoteType, RemoteStateType import localizedFormat from 'dayjs/plugin/localizedFormat'; import {GlobalModel, GlobalCommandRunner, Session, Cmd, Window, Screen, ScreenWindow, riToRPtr, windowWidthToCols, windowHeightToRows, termHeightFromRows, termWidthFromCols} from "./model"; import {isModKeyPress} from "./util"; +import {ImageRendererModel} from "./imagerenderer"; dayjs.extend(localizedFormat) @@ -29,6 +30,9 @@ type OMap = mobx.ObservableMap; type HeightChangeCallbackType = (lineNum : number, newHeight : number, oldHeight : number) => void; +type RendererComponentProps = {sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType}; +type RendererComponentType = { new(props : RendererComponentProps) : React.Component }; + type InterObsValue = { sessionid : string, windowid : string, @@ -213,6 +217,114 @@ class Prompt extends React.Component<{rptr : RemotePtrType, festate : FeStateTyp } } +@mobxReact.observer +class ImageRenderer extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : () => void}, {}> { + elemRef : React.RefObject = React.createRef(); + imageDivRef : React.RefObject = React.createRef(); + imageLoaded : mobx.IObservableValue = mobx.observable.box(false, {name: "imageLoaded"}); + imageModel : ImageRendererModel; + + constructor(props) { + super(props); + } + + componentDidMount() { + this.componentDidUpdate(null, null, null); + } + + componentWillUnmount() { + if (this.imageLoaded.get()) { + this.unloadImage(true); + } + } + + getSnapshotBeforeUpdate(prevProps, prevState) : {height : number} { + let elem = this.elemRef.current; + if (elem == null) { + return {height: 0}; + } + return {height: elem.offsetHeight}; + } + + componentDidUpdate(prevProps, prevState, snapshot : {height : number}) : void { + if (this.props.onHeightChange == null) { + return; + } + let {line} = this.props; + let curHeight = 0; + let elem = this.elemRef.current; + if (elem != null) { + curHeight = elem.offsetHeight; + } + if (snapshot == null) { + snapshot = {height: 0}; + } + if (snapshot.height != curHeight) { + this.props.onHeightChange(); + // console.log("image-render height change: ", line.linenum, snapshot.height, "=>", curHeight); + } + this.checkLoad(); + } + + checkLoad() : void { + let {line, staticRender, visible} = this.props; + if (staticRender) { + return; + } + let vis = visible && visible.get(); + let curVis = this.imageLoaded.get(); + if (vis && !curVis) { + this.loadImage(); + } + else if (!vis && curVis) { + this.unloadImage(false); + } + } + + loadImage() : void { + let {sw, line} = this.props; + let model = GlobalModel; + let cmd = model.getCmd(line); + if (cmd == null) { + return; + } + let elem = this.imageDivRef.current; + if (elem == null) { + console.log("cannot load image, no elem found"); + return; + } + this.imageModel = sw.loadImageRenderer(this.imageDivRef.current, line, cmd); + mobx.action(() => this.imageLoaded.set(true))(); + } + + unloadImage(unmount : boolean) : void { + let {sw, line} = this.props; + sw.unloadRenderer(line.cmdid); + this.imageModel = null; + if (!unmount) { + mobx.action(() => this.imageLoaded.set(false))(); + if (this.imageDivRef.current != null) { + this.imageDivRef.current.replaceChildren(); + } + } + } + + render() { + let imageModel = this.imageModel; + let isLoaded = this.imageLoaded.get(); + let isDone = (imageModel != null && imageModel.isDone.get()); + if (imageModel != null) { + let dataVersion = imageModel.dataVersion.get(); + } + return ( +
+
+
...
+
+ ); + } +} + @mobxReact.observer class TerminalRenderer extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : () => void}, {}> { termLoaded : mobx.IObservableValue = mobx.observable.box(false, {name: "linecmd-term-loaded"}); @@ -545,6 +657,10 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width {"cmd-done": !cmd.isRunning()}, {"has-rtnstate": cmd.getRtnState()}, ); + let RendererComponent : RendererComponentType = TerminalRenderer; + if (line.renderer == "image") { + RendererComponent = ImageRenderer; + } return (
- +
diff --git a/src/model.ts b/src/model.ts index ffd9965c..e907af6d 100644 --- a/src/model.ts +++ b/src/model.ts @@ -7,7 +7,7 @@ import {TermWrap} from "./term"; import {v4 as uuidv4} from "uuid"; import type {SessionDataType, WindowDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, RemotePtrType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenWindowType, ScreenOptsType, LayoutType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, TermWinSize, RemoteInputPacketType, FeStateType, ContextMenuOpts, RendererContext, RendererModel, PtyDataType} from "./types"; import {WSControl} from "./ws"; -import {ImageRenderer} from "./imagerenderer"; +import {ImageRendererModel} from "./imagerenderer"; var GlobalUser = "sawka"; const DefaultCellWidth = 7.203125; @@ -550,6 +550,14 @@ class ScreenWindow { return false; } + loadImageRenderer(imageDivElem : any, line : LineType, cmd : Cmd) : ImageRendererModel { + let cmdId = cmd.cmdId; + let context = {sessionId: this.sessionId, screenId: this.screenId, windowId: this.windowId, cmdId: cmdId, lineId : line.lineid, lineNum: line.linenum}; + let imageModel = new ImageRendererModel(imageDivElem, context, cmd.getTermOpts(), !cmd.isRunning()); + this.renderers[cmdId] = imageModel; + return imageModel; + } + loadTerminalRenderer(elem : Element, line : LineType, cmd : Cmd, width : number) { let cmdId = cmd.cmdId; let termWrap = this.getRenderer(cmdId); diff --git a/src/sh2.less b/src/sh2.less index fa67bde3..ccacb014 100644 --- a/src/sh2.less +++ b/src/sh2.less @@ -474,6 +474,17 @@ html, body, #main { margin-bottom: -5px; } + .image-wrapper { + padding: 2px 10px 5px 4px; + margin: 4px 8px 0 -4px; + + .loading-div { + height: 20px; + margin-left: 50px; + .mono-font(); + } + } + .terminal-wrapper { background-color: #000; padding: 2px 10px 5px 4px; diff --git a/src/types.ts b/src/types.ts index 67aa4812..817063e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,6 +26,7 @@ type LineType = { linelocal : boolean, linetype : string, text : string, + renderer : string, cmdid? : string, contentheight? : number, star? : number, diff --git a/src/util.ts b/src/util.ts index de13fc73..798169fb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -176,4 +176,10 @@ function isModKeyPress(e : any) { return e.code.match(/^(Control|Meta|Alt|Shift)(Left|Right)$/); } -export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeSimpleData, parseEnv0, boundInt, isModKeyPress}; +function incObs(inum : mobx.IObservableValue) { + mobx.action(() => { + inum.set(inum.get() + 1); + })(); +} + +export {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeSimpleData, parseEnv0, boundInt, isModKeyPress, incObs};