Add header bar component #6
| @ -1,3 +1,27 @@ | |||||||
| div.header-bar { | div.header-bar { | ||||||
|  |     padding: 0 20px; | ||||||
|  |     box-sizing: border-box; | ||||||
|     display: flex; |     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; | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| @ -1,37 +1,126 @@ | |||||||
| import { Component, ReactNode } from "react"; | import { Component, ReactNode } from "react"; | ||||||
| import { useStatus, IMixinStatusProps } from "@Context/Status"; | import { useStatus, IMixinStatusProps } from "@Context/Status"; | ||||||
|  | import { useSetting, IMixinSettingProps } from "@Context/Setting"; | ||||||
| import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme"; | 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 { | interface IHeaderBarProps { | ||||||
|     height: number; |     height: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | interface HeaderBarState { | ||||||
|  |     renderFps: number; | ||||||
|  |     physicsFps: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * 头部信息栏 |  * 头部信息栏 | ||||||
|  */ |  */ | ||||||
|  | @useSetting | ||||||
| @useStatus | @useStatus | ||||||
| class HeaderBar extends Component<IHeaderBarProps & IMixinStatusProps> { | class HeaderBar extends Component< | ||||||
|  |     IHeaderBarProps & IMixinStatusProps & IMixinSettingProps, | ||||||
|  |     HeaderBarState | ||||||
|  | > { | ||||||
|  | 
 | ||||||
|  |     public state = { | ||||||
|  |         renderFps: 0, | ||||||
|  |         physicsFps: 0, | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     private changeListener = () => { |     private changeListener = () => { | ||||||
|         this.forceUpdate(); |         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() { |     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 { |     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 <Theme |         return <Theme | ||||||
|             className="header-bar" |             className="header-bar" | ||||||
|             backgroundLevel={BackgroundLevel.Level1} |             backgroundLevel={BackgroundLevel.Level1} | ||||||
|             fontLevel={FontLevel.Level3} |             fontLevel={FontLevel.Level3} | ||||||
|             style={{ height: this.props.height }} |             style={{ height: this.props.height }} | ||||||
|         > |         > | ||||||
|             Living Together | Web |             <TooltipHost content={I18N(this.props, "Header.Bar.Title.Info")}> | ||||||
|  |                 <div className="title"> | ||||||
|  |                     <Icon iconName="HomeGroup"></Icon> | ||||||
|  |                     <span>{I18N(this.props, "Header.Bar.Title")}</span> | ||||||
|  |                 </div> | ||||||
|  |             </TooltipHost> | ||||||
|  |             <TooltipHost content={I18N(this.props, "Header.Bar.File.Name.Info", { | ||||||
|  |                 file: isNewFile ? I18N(this.props, "Header.Bar.New.File.Name") : fileName, | ||||||
|  |                 status: isSaved ? I18N(this.props, "Header.Bar.File.Save.Status.Saved") :  | ||||||
|  |                     I18N(this.props, "Header.Bar.File.Save.Status.Unsaved") | ||||||
|  |             })}> | ||||||
|  |                 <div className="file-name">{ | ||||||
|  |                     isNewFile ? I18N(this.props, "Header.Bar.New.File.Name") : fileName | ||||||
|  |                 }{ | ||||||
|  |                     isSaved ? "" : "*" | ||||||
|  |                 }</div> | ||||||
|  |             </TooltipHost> | ||||||
|  |             <TooltipHost content={I18N(this.props, "Header.Bar.Fps.Info", fpsInfo)}> | ||||||
|  |                 <div className="fps-view"> | ||||||
|  |                     <Icon iconName="SpeedHigh"></Icon> | ||||||
|  |                     <span>{I18N(this.props, "Header.Bar.Fps", fpsInfo)}</span> | ||||||
|  |                 </div> | ||||||
|  |             </TooltipHost> | ||||||
|         </Theme> |         </Theme> | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -15,8 +15,14 @@ interface ILocalizationProps { | |||||||
|     options?: Record<string, string>; |     options?: Record<string, string>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function I18N(language: Language, key: keyof typeof EN_US, values?: Record<string, string>) { | function I18N(language: Language | IMixinSettingProps, key: keyof typeof EN_US, values?: Record<string, string>) { | ||||||
|     let i18nValue = LanguageDataBase[language][key]; |     let lang: Language; | ||||||
|  |     if (typeof language === "string") { | ||||||
|  |         lang = language; | ||||||
|  |     } else { | ||||||
|  |         lang = language.setting?.language ?? "EN_US"; | ||||||
|  |     } | ||||||
|  |     let i18nValue = LanguageDataBase[lang][key]; | ||||||
|     if (values) { |     if (values) { | ||||||
|         for (let valueKey in values) { |         for (let valueKey in values) { | ||||||
|             i18nValue = i18nValue.replaceAll(new RegExp(`\\{\\s*${valueKey}\\s*\\}`, "g"), values[valueKey]); |             i18nValue = i18nValue.replaceAll(new RegExp(`\\{\\s*${valueKey}\\s*\\}`, "g"), values[valueKey]); | ||||||
|  | |||||||
| @ -18,10 +18,10 @@ $lt-bg-color-lvl4-dark: $ms-color-gray180; | |||||||
| $lt-bg-color-lvl5-dark: $ms-color-gray200; | $lt-bg-color-lvl5-dark: $ms-color-gray200; | ||||||
| 
 | 
 | ||||||
| // 文字颜色 | // 文字颜色 | ||||||
| $lt-font-color-normal-dark: $ms-color-gray110; | $lt-font-color-normal-dark: $ms-color-gray90; | ||||||
| $lt-font-color-lvl3-dark: $ms-color-gray100; | $lt-font-color-lvl3-dark: $ms-color-gray80; | ||||||
| $lt-font-color-lvl2-dark: $ms-color-gray100; | $lt-font-color-lvl2-dark: $ms-color-gray80; | ||||||
| $lt-font-color-lvl1-dark: $ms-color-gray90; | $lt-font-color-lvl1-dark: $ms-color-gray70; | ||||||
| 
 | 
 | ||||||
| // 背景颜色 | // 背景颜色 | ||||||
| $lt-bg-color-lvl1-light: $ms-color-gray10; | $lt-bg-color-lvl1-light: $ms-color-gray10; | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ abstract class BasicRenderer< | |||||||
|     P extends IRendererParam = {}, |     P extends IRendererParam = {}, | ||||||
|     M extends IAnyObject = {}, |     M extends IAnyObject = {}, | ||||||
|     E extends Record<EventType, any> = {} |     E extends Record<EventType, any> = {} | ||||||
| > extends AbstractRenderer<P, M & IRendererParams, E> { | > extends AbstractRenderer<P, M & IRendererParams, E & {loop: number}> { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| 	 * 渲染器参数 | 	 * 渲染器参数 | ||||||
|  | |||||||
| @ -75,6 +75,8 @@ class ClassicRenderer extends BasicRenderer<{}, IClassicRendererParams> { | |||||||
| 
 | 
 | ||||||
|     loop(t: number): void { |     loop(t: number): void { | ||||||
| 
 | 
 | ||||||
|  |         this.emit("loop", t); | ||||||
|  | 
 | ||||||
|         // 常规绘制窗口
 |         // 常规绘制窗口
 | ||||||
|         this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); |         this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,13 @@ | |||||||
| const EN_US = { | const EN_US = { | ||||||
|     "EN_US": "English (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; | export default EN_US; | ||||||
| @ -1,5 +1,13 @@ | |||||||
| const ZH_CN = { | const ZH_CN = { | ||||||
|     "EN_US": "英语 (美国)", |     "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; | export default ZH_CN; | ||||||
| @ -20,6 +20,11 @@ class Archive< | |||||||
|      */ |      */ | ||||||
|     public fileName?: string; |     public fileName?: string; | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * 是否保存 | ||||||
|  |      */ | ||||||
|  |     public isSaved: boolean = false; | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * 文件数据 |      * 文件数据 | ||||||
|      */ |      */ | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import { ObjectID, AbstractRenderer } from "./Renderer"; | |||||||
| import { Label } from "./Label"; | import { Label } from "./Label"; | ||||||
| 
 | 
 | ||||||
| type ModelEvent = { | type ModelEvent = { | ||||||
|  |     loop: number; | ||||||
|     groupAdd: Group; |     groupAdd: Group; | ||||||
|     rangeAdd: Range; |     rangeAdd: Range; | ||||||
|     labelAdd: Label; |     labelAdd: Label; | ||||||
| @ -205,6 +206,8 @@ class Model extends Emitter<ModelEvent> { | |||||||
|                 } as any); |                 } as any); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         this.emit("loop", t); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -46,10 +46,15 @@ interface IRendererConstructor< | |||||||
| 	M extends IAnyObject = {} | 	M extends IAnyObject = {} | ||||||
| > { | > { | ||||||
| 	new (canvas: HTMLCanvasElement, param?: M): AbstractRenderer< | 	new (canvas: HTMLCanvasElement, param?: M): AbstractRenderer< | ||||||
| 		IRendererParam, IAnyObject, Record<EventType, any> | 		IRendererParam, IAnyObject, AbstractRendererEvent | ||||||
| 	> | 	> | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type AbstractRendererEvent = { | ||||||
|  |     [x: EventType]: any; | ||||||
|  |     loop: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * 渲染器 API |  * 渲染器 API | ||||||
|  * @template P 渲染器绘制参数 |  * @template P 渲染器绘制参数 | ||||||
| @ -59,7 +64,7 @@ interface IRendererConstructor< | |||||||
| abstract class AbstractRenderer< | abstract class AbstractRenderer< | ||||||
| 	P extends IRendererParam = {}, | 	P extends IRendererParam = {}, | ||||||
| 	M extends IAnyObject = {}, | 	M extends IAnyObject = {}, | ||||||
| 	E extends Record<EventType, any> = {} | 	E extends AbstractRendererEvent = {loop: number} | ||||||
| > extends Emitter<E> { | > extends Emitter<E> { | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
|  | |||||||
| @ -6,8 +6,11 @@ import { Localization } from "@Component/Localization/Localization"; | |||||||
| import { Entry } from "../Entry/Entry"; | import { Entry } from "../Entry/Entry"; | ||||||
| import { StatusProvider, Status } from "@Context/Status"; | import { StatusProvider, Status } from "@Context/Status"; | ||||||
| import { ClassicRenderer } from "@GLRender/ClassicRenderer"; | import { ClassicRenderer } from "@GLRender/ClassicRenderer"; | ||||||
|  | import { initializeIcons } from '@fluentui/font-icons-mdl2'; | ||||||
| import "./SimulatorWeb.scss"; | import "./SimulatorWeb.scss"; | ||||||
| 
 | 
 | ||||||
|  | initializeIcons(); | ||||||
|  | 
 | ||||||
| class SimulatorWeb extends Component { | class SimulatorWeb extends Component { | ||||||
|      |      | ||||||
|     /** |     /** | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user