diff --git a/source/Component/HeaderBar/HeaderBar.scss b/source/Component/HeaderBar/HeaderBar.scss index 7fa55be..f7cda1d 100644 --- a/source/Component/HeaderBar/HeaderBar.scss +++ b/source/Component/HeaderBar/HeaderBar.scss @@ -1,3 +1,27 @@ div.header-bar { + padding: 0 20px; + box-sizing: border-box; display: flex; + align-items: center; + justify-content: space-between; + user-select: none; + + div.title > i, div.fps-view > i { + font-size: larger; + vertical-align: text-bottom; + padding-right: 5px; + } + + div.ms-TooltipHost { + padding: 0 5px; + overflow: hidden; + flex-shrink: 1; + + div { + overflow: hidden; + text-overflow: ellipsis; + word-break: keep-all; + white-space: nowrap; + } + } } \ No newline at end of file diff --git a/source/Component/HeaderBar/HeaderBar.tsx b/source/Component/HeaderBar/HeaderBar.tsx index 86bc381..ce5a925 100644 --- a/source/Component/HeaderBar/HeaderBar.tsx +++ b/source/Component/HeaderBar/HeaderBar.tsx @@ -1,37 +1,126 @@ import { Component, ReactNode } from "react"; import { useStatus, IMixinStatusProps } from "@Context/Status"; +import { useSetting, IMixinSettingProps } from "@Context/Setting"; import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme"; +import { Icon } from '@fluentui/react/lib/Icon'; +import { I18N } from "../Localization/Localization"; +import "./HeaderBar.scss"; +import { Tooltip, TooltipHost } from "@fluentui/react"; interface IHeaderBarProps { height: number; } +interface HeaderBarState { + renderFps: number; + physicsFps: number; +} + /** * 头部信息栏 */ +@useSetting @useStatus -class HeaderBar extends Component { +class HeaderBar extends Component< + IHeaderBarProps & IMixinStatusProps & IMixinSettingProps, + HeaderBarState +> { + + public state = { + renderFps: 0, + physicsFps: 0, + } private changeListener = () => { this.forceUpdate(); } - public componentDidMount() { + private updateTime: number = 0; + private createFpsCalc(type: "renderFps" | "physicsFps") { + return (t: number) => { + let newState: HeaderBarState = {} as any; + newState[type] = 1 / t; + if (this.updateTime > 60) { + this.updateTime = 0; + this.setState(newState); + } + this.updateTime ++; + } + } + + private renderFpsCalc: (t: number) => void = () => {}; + private physicsFpsCalc: (t: number) => void = () => {}; + + public componentDidMount() { + const { setting, status } = this.props; + this.renderFpsCalc = this.createFpsCalc("renderFps"); + this.physicsFpsCalc = this.createFpsCalc("physicsFps"); + if (setting) { + setting.on("language", this.changeListener); + } + if (status) { + status.model.on("loop", this.physicsFpsCalc); + status.renderer.on("loop", this.renderFpsCalc); + } } public componentWillUnmount() { - + const { setting, status } = this.props; + if (setting) { + setting.off("language", this.changeListener); + } + if (status) { + status.model.off("loop", this.physicsFpsCalc); + status.renderer.off("loop", this.renderFpsCalc); + } } public render(): ReactNode { + const { status } = this.props; + let fileName: string = ""; + let isNewFile: boolean = true; + let isSaved: boolean = false; + if (status) { + isNewFile = status.archive.isNewFile; + fileName = status.archive.fileName ?? ""; + isSaved = status.archive.isSaved; + } + + const fpsInfo = { + renderFps: Math.floor(this.state.renderFps).toString(), + physicsFps: Math.floor(this.state.physicsFps).toString() + }; + return - Living Together | Web + +
+ + {I18N(this.props, "Header.Bar.Title")} +
+
+ +
{ + isNewFile ? I18N(this.props, "Header.Bar.New.File.Name") : fileName + }{ + isSaved ? "" : "*" + }
+
+ +
+ + {I18N(this.props, "Header.Bar.Fps", fpsInfo)} +
+
} } diff --git a/source/Component/Localization/Localization.tsx b/source/Component/Localization/Localization.tsx index 4c24e01..e187289 100644 --- a/source/Component/Localization/Localization.tsx +++ b/source/Component/Localization/Localization.tsx @@ -15,8 +15,14 @@ interface ILocalizationProps { options?: Record; } -function I18N(language: Language, key: keyof typeof EN_US, values?: Record) { - let i18nValue = LanguageDataBase[language][key]; +function I18N(language: Language | IMixinSettingProps, key: keyof typeof EN_US, values?: Record) { + let lang: Language; + if (typeof language === "string") { + lang = language; + } else { + lang = language.setting?.language ?? "EN_US"; + } + let i18nValue = LanguageDataBase[lang][key]; if (values) { for (let valueKey in values) { i18nValue = i18nValue.replaceAll(new RegExp(`\\{\\s*${valueKey}\\s*\\}`, "g"), values[valueKey]); diff --git a/source/Component/Theme/Theme.scss b/source/Component/Theme/Theme.scss index c714672..a4b95b8 100644 --- a/source/Component/Theme/Theme.scss +++ b/source/Component/Theme/Theme.scss @@ -18,10 +18,10 @@ $lt-bg-color-lvl4-dark: $ms-color-gray180; $lt-bg-color-lvl5-dark: $ms-color-gray200; // 文字颜色 -$lt-font-color-normal-dark: $ms-color-gray110; -$lt-font-color-lvl3-dark: $ms-color-gray100; -$lt-font-color-lvl2-dark: $ms-color-gray100; -$lt-font-color-lvl1-dark: $ms-color-gray90; +$lt-font-color-normal-dark: $ms-color-gray90; +$lt-font-color-lvl3-dark: $ms-color-gray80; +$lt-font-color-lvl2-dark: $ms-color-gray80; +$lt-font-color-lvl1-dark: $ms-color-gray70; // 背景颜色 $lt-bg-color-lvl1-light: $ms-color-gray10; diff --git a/source/GLRender/BasicRenderer.ts b/source/GLRender/BasicRenderer.ts index 115cf27..efa4306 100644 --- a/source/GLRender/BasicRenderer.ts +++ b/source/GLRender/BasicRenderer.ts @@ -18,7 +18,7 @@ abstract class BasicRenderer< P extends IRendererParam = {}, M extends IAnyObject = {}, E extends Record = {} -> extends AbstractRenderer { +> extends AbstractRenderer { /** * 渲染器参数 diff --git a/source/GLRender/ClassicRenderer.ts b/source/GLRender/ClassicRenderer.ts index ed6dadf..6d310ea 100644 --- a/source/GLRender/ClassicRenderer.ts +++ b/source/GLRender/ClassicRenderer.ts @@ -75,6 +75,8 @@ class ClassicRenderer extends BasicRenderer<{}, IClassicRendererParams> { loop(t: number): void { + this.emit("loop", t); + // 常规绘制窗口 this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index c78cf27..aec7bf2 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -1,5 +1,13 @@ const EN_US = { "EN_US": "English (US)", - "ZH_CN": "Chinese (Simplified)" + "ZH_CN": "Chinese (Simplified)", + "Header.Bar.Title": "Living Together | Emulator", + "Header.Bar.Title.Info": "Group Behavior Research Emulator", + "Header.Bar.File.Name.Info": "{file} ({status})", + "Header.Bar.New.File.Name": "New File", + "Header.Bar.File.Save.Status.Saved": "Saved", + "Header.Bar.File.Save.Status.Unsaved": "UnSaved", + "Header.Bar.Fps": "FPS: {renderFps} | {physicsFps}", + "Header.Bar.Fps.Info": "The rendering frame rate ({renderFps} / fps) is on the left, and the simulation frame rate ({physicsFps} / fps) is on the right.", } export default EN_US; \ No newline at end of file diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index 884ef70..36fdfad 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -1,5 +1,13 @@ const ZH_CN = { "EN_US": "英语 (美国)", - "ZH_CN": "中文 (简体)" + "ZH_CN": "中文 (简体)", + "Header.Bar.Title": "群生共进 | 仿真器", + "Header.Bar.Title.Info": "群体行为研究仿真器", + "Header.Bar.File.Name.Info": "{file} ({status})", + "Header.Bar.New.File.Name": "新存档", + "Header.Bar.File.Save.Status.Saved": "已保存", + "Header.Bar.File.Save.Status.Unsaved": "未保存", + "Header.Bar.Fps": "帧率: {renderFps} | {physicsFps}", + "Header.Bar.Fps.Info": "左侧为渲染帧率 ({renderFps} / fps), 右侧为模拟帧率 ({physicsFps} / fps)。", } export default ZH_CN; \ No newline at end of file diff --git a/source/Model/Archive.ts b/source/Model/Archive.ts index 0c0da10..b29a7fd 100644 --- a/source/Model/Archive.ts +++ b/source/Model/Archive.ts @@ -20,6 +20,11 @@ class Archive< */ public fileName?: string; + /** + * 是否保存 + */ + public isSaved: boolean = false; + /** * 文件数据 */ diff --git a/source/Model/Model.ts b/source/Model/Model.ts index 3f17d78..22dcf17 100644 --- a/source/Model/Model.ts +++ b/source/Model/Model.ts @@ -8,6 +8,7 @@ import { ObjectID, AbstractRenderer } from "./Renderer"; import { Label } from "./Label"; type ModelEvent = { + loop: number; groupAdd: Group; rangeAdd: Range; labelAdd: Label; @@ -205,6 +206,8 @@ class Model extends Emitter { } as any); } } + + this.emit("loop", t); } } diff --git a/source/Model/Renderer.ts b/source/Model/Renderer.ts index a805681..02a992f 100644 --- a/source/Model/Renderer.ts +++ b/source/Model/Renderer.ts @@ -46,10 +46,15 @@ interface IRendererConstructor< M extends IAnyObject = {} > { new (canvas: HTMLCanvasElement, param?: M): AbstractRenderer< - IRendererParam, IAnyObject, Record + IRendererParam, IAnyObject, AbstractRendererEvent > } +type AbstractRendererEvent = { + [x: EventType]: any; + loop: number; +} + /** * 渲染器 API * @template P 渲染器绘制参数 @@ -59,7 +64,7 @@ interface IRendererConstructor< abstract class AbstractRenderer< P extends IRendererParam = {}, M extends IAnyObject = {}, - E extends Record = {} + E extends AbstractRendererEvent = {loop: number} > extends Emitter { /** diff --git a/source/Page/SimulatorWeb/SimulatorWeb.tsx b/source/Page/SimulatorWeb/SimulatorWeb.tsx index 4219e70..6c1c81d 100644 --- a/source/Page/SimulatorWeb/SimulatorWeb.tsx +++ b/source/Page/SimulatorWeb/SimulatorWeb.tsx @@ -6,8 +6,11 @@ import { Localization } from "@Component/Localization/Localization"; import { Entry } from "../Entry/Entry"; import { StatusProvider, Status } from "@Context/Status"; import { ClassicRenderer } from "@GLRender/ClassicRenderer"; +import { initializeIcons } from '@fluentui/font-icons-mdl2'; import "./SimulatorWeb.scss"; +initializeIcons(); + class SimulatorWeb extends Component { /**