Ported quest info view to the new GUI system.

This commit is contained in:
Daan Vanden Bosch 2019-08-23 17:00:39 +02:00
parent 8e13441f26
commit 17400200a0
38 changed files with 512 additions and 181 deletions

View File

@ -1,10 +1,10 @@
import { NavigationView } from "./NavigationView"; import { NavigationView } from "./NavigationView";
import { MainContentView } from "./MainContentView"; import { MainContentView } from "./MainContentView";
import { el } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableView } from "../../core/gui/ResizableView";
export class ApplicationView extends ResizableView { export class ApplicationView extends ResizableView {
element = el("div", { class: "application_ApplicationView" }); element = create_element("div", { class: "application_ApplicationView" });
private menu_view = this.disposable(new NavigationView()); private menu_view = this.disposable(new NavigationView());
private main_content_view = this.disposable(new MainContentView()); private main_content_view = this.disposable(new MainContentView());

View File

@ -1,4 +1,4 @@
import { el } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { gui_store, GuiTool } from "../../core/stores/GuiStore";
import { LazyView } from "../../core/gui/LazyView"; import { LazyView } from "../../core/gui/LazyView";
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableView } from "../../core/gui/ResizableView";
@ -12,7 +12,7 @@ const TOOLS: [GuiTool, () => Promise<ResizableView>][] = [
]; ];
export class MainContentView extends ResizableView { export class MainContentView extends ResizableView {
element = el("div", { class: "application_MainContentView" }); element = create_element("div", { class: "application_MainContentView" });
private tool_views = new Map( private tool_views = new Map(
TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyView(create_view))]), TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyView(create_view))]),
@ -26,7 +26,7 @@ export class MainContentView extends ResizableView {
} }
const tool_view = this.tool_views.get(gui_store.tool.val); const tool_view = this.tool_views.get(gui_store.tool.val);
if (tool_view) tool_view.visible = true; if (tool_view) tool_view.visible.val = true;
this.disposable(gui_store.tool.observe(this.tool_changed)); this.disposable(gui_store.tool.observe(this.tool_changed));
} }
@ -43,10 +43,10 @@ export class MainContentView extends ResizableView {
private tool_changed = (new_tool: GuiTool) => { private tool_changed = (new_tool: GuiTool) => {
for (const tool of this.tool_views.values()) { for (const tool of this.tool_views.values()) {
tool.visible = false; tool.visible.val = false;
} }
const new_view = this.tool_views.get(new_tool); const new_view = this.tool_views.get(new_tool);
if (new_view) new_view.visible = true; if (new_view) new_view.visible.val = true;
}; };
} }

View File

@ -16,7 +16,7 @@
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
font-size: 15px; font-size: 13px;
height: 100%; height: 100%;
padding: 0 20px; padding: 0 20px;
color: hsl(0, 0%, 65%); color: hsl(0, 0%, 65%);

View File

@ -1,4 +1,4 @@
import { el } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import "./NavigationView.css"; import "./NavigationView.css";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { gui_store, GuiTool } from "../../core/stores/GuiStore";
import { View } from "../../core/gui/View"; import { View } from "../../core/gui/View";
@ -10,7 +10,7 @@ const TOOLS: [GuiTool, string][] = [
]; ];
export class NavigationView extends View { export class NavigationView extends View {
readonly element = el("div", { class: "application_NavigationView" }); readonly element = create_element("div", { class: "application_NavigationView" });
readonly height = 30; readonly height = 30;
@ -22,7 +22,7 @@ export class NavigationView extends View {
super(); super();
this.element.style.height = `${this.height}px`; this.element.style.height = `${this.height}px`;
this.element.onclick = this.click; this.element.onmousedown = this.mousedown;
for (const button of this.buttons.values()) { for (const button of this.buttons.values()) {
this.element.append(button.element); this.element.append(button.element);
@ -32,7 +32,7 @@ export class NavigationView extends View {
this.disposable(gui_store.tool.observe(this.tool_changed)); this.disposable(gui_store.tool.observe(this.tool_changed));
} }
private click(e: MouseEvent): void { private mousedown(e: MouseEvent): void {
if (e.target instanceof HTMLLabelElement && e.target.control instanceof HTMLInputElement) { if (e.target instanceof HTMLLabelElement && e.target.control instanceof HTMLInputElement) {
gui_store.tool.val = (GuiTool as any)[e.target.control.value]; gui_store.tool.val = (GuiTool as any)[e.target.control.value];
} }
@ -45,10 +45,10 @@ export class NavigationView extends View {
} }
class ToolButton extends View { class ToolButton extends View {
element: HTMLElement = el("span"); element: HTMLElement = create_element("span");
private input: HTMLInputElement = el("input"); private input: HTMLInputElement = create_element("input");
private label: HTMLLabelElement = el("label"); private label: HTMLLabelElement = create_element("label");
constructor(tool: GuiTool, text: string) { constructor(tool: GuiTool, text: string) {
super(); super();

View File

@ -2,15 +2,19 @@
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
align-content: stretch;
box-sizing: border-box; box-sizing: border-box;
height: 26px;
padding: 0; padding: 0;
border: solid 1px hsl(0, 0%, 10%); border: solid 1px hsl(0, 0%, 10%);
color: hsl(0, 0%, 80%); color: hsl(0, 0%, 80%);
outline: none; outline: none;
font-size: 13px;
} }
.core_Button .core_Button_inner { .core_Button .core_Button_inner {
display: flex; flex: 1;
display: inline-flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;

View File

@ -1,11 +1,11 @@
import { el } from "./dom"; import { create_element } from "./dom";
import "./Button.css"; import "./Button.css";
import { Observable } from "../observable/Observable"; import { Observable } from "../observable/Observable";
import { emitter } from "../observable"; import { emitter } from "../observable";
import { Control } from "./Control"; import { Control } from "./Control";
export class Button extends Control { export class Button extends Control {
readonly element: HTMLButtonElement = el("button", { class: "core_Button" }); readonly element: HTMLButtonElement = create_element("button", { class: "core_Button" });
private readonly _click = emitter<MouseEvent>(); private readonly _click = emitter<MouseEvent>();
readonly click: Observable<MouseEvent> = this._click; readonly click: Observable<MouseEvent> = this._click;
@ -13,7 +13,7 @@ export class Button extends Control {
constructor(text: string) { constructor(text: string) {
super(); super();
this.element.append(el("span", { class: "core_Button_inner", text })); this.element.append(create_element("span", { class: "core_Button_inner", text }));
this.enabled.observe(enabled => (this.element.disabled = !enabled)); this.enabled.observe(enabled => (this.element.disabled = !enabled));

View File

@ -1,10 +1,10 @@
import { el } from "./dom"; import { create_element } from "./dom";
import { WritableProperty } from "../observable/WritableProperty"; import { WritableProperty } from "../observable/WritableProperty";
import { property } from "../observable"; import { property } from "../observable";
import { LabelledControl } from "./LabelledControl"; import { LabelledControl } from "./LabelledControl";
export class CheckBox extends LabelledControl { export class CheckBox extends LabelledControl {
readonly element: HTMLInputElement = el("input", { class: "core_CheckBox" }); readonly element: HTMLInputElement = create_element("input", { class: "core_CheckBox" });
readonly checked: WritableProperty<boolean> = property(false); readonly checked: WritableProperty<boolean> = property(false);

View File

@ -1,4 +1,4 @@
import { el } from "./dom"; import { create_element } from "./dom";
import "./FileButton.css"; import "./FileButton.css";
import "./Button.css"; import "./Button.css";
import { property } from "../observable"; import { property } from "../observable";
@ -6,14 +6,14 @@ import { Property } from "../observable/Property";
import { Control } from "./Control"; import { Control } from "./Control";
export class FileButton extends Control { export class FileButton extends Control {
readonly element: HTMLLabelElement = el("label", { readonly element: HTMLLabelElement = create_element("label", {
class: "core_FileButton core_Button", class: "core_FileButton core_Button",
}); });
private readonly _files = property<File[]>([]); private readonly _files = property<File[]>([]);
readonly files: Property<File[]> = this._files; readonly files: Property<File[]> = this._files;
private input: HTMLInputElement = el("input", { private input: HTMLInputElement = create_element("input", {
class: "core_FileButton_input core_Button_inner", class: "core_FileButton_input core_Button_inner",
}); });
@ -31,7 +31,7 @@ export class FileButton extends Control {
}; };
this.element.append( this.element.append(
el("span", { create_element("span", {
class: "core_FileButton_inner core_Button_inner", class: "core_FileButton_inner core_Button_inner",
text, text,
}), }),

View File

@ -1,32 +1,35 @@
.core_Input { .core_Input {
display: inline-block;
box-sizing: border-box; box-sizing: border-box;
border: solid 1px hsl(0, 0%, 25%); height: 24px;
border: var(--input-border);
} }
.core_Input .core_Input_inner { .core_Input .core_Input_inner {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
height: 24px; height: 100%;
padding: 0 3px; padding: 0 3px;
border: solid 1px hsl(0, 0%, 0%); border: var(--input-inner-border);
background-color: hsl(0, 0%, 12%); background-color: var(--input-bg-color);
color: hsl(0, 0%, 75%); color: var(--input-text-color);
outline: none; outline: none;
font-size: 13px;
} }
.core_Input:hover { .core_Input:hover {
border-color: hsl(0, 0%, 35%); border-color: var(--input-border-hover);
} }
.core_Input:focus-within { .core_Input:focus-within {
border-color: hsl(0, 0%, 45%); border-color: var(--input-border-focus);
} }
.core_Input.disabled { .core_Input.disabled {
border: solid 1px hsl(0, 0%, 20%); border: var(--input-border-disabled);
} }
.core_Input.disabled .core_Input_inner { .core_Input.disabled .core_Input_inner {
background-color: hsl(0, 0%, 15%); color: var(--input-text-color-disabled);
color: var(--text-color-disabled); background-color: var(--input-bg-color-disabled);
} }

83
src/core/gui/Input.ts Normal file
View File

@ -0,0 +1,83 @@
/* eslint-disable no-dupe-class-members */
import { LabelledControl } from "./LabelledControl";
import { create_element } from "./dom";
import { WritableProperty } from "../observable/WritableProperty";
import { is_any_property, Property } from "../observable/Property";
import "./Input.css";
export abstract class Input<T> extends LabelledControl {
readonly element: HTMLElement;
readonly value: WritableProperty<T>;
protected readonly input: HTMLInputElement;
protected constructor(
value: WritableProperty<T>,
class_name: string,
input_type: string,
input_class_name: string,
label?: string,
) {
super(label);
this.value = value;
this.element = create_element("span", { class: `${class_name} core_Input` });
this.input = create_element("input", {
class: `${input_class_name} core_Input_inner`,
});
this.input.type = input_type;
this.input.onchange = () => (this.value.val = this.get_input_value());
this.set_input_value(value.val);
this.element.append(this.input);
this.disposables(
this.value.observe(value => this.set_input_value(value)),
this.enabled.observe(enabled => {
this.input.disabled = !enabled;
if (enabled) {
this.element.classList.remove("disabled");
} else {
this.element.classList.add("disabled");
}
}),
);
}
protected abstract get_input_value(): T;
protected abstract set_input_value(value: T): void;
protected set_attr<T>(attr: InputAttrsOfType<T>, value?: T | Property<T>): void;
protected set_attr<T, U>(
attr: InputAttrsOfType<U>,
value: T | Property<T> | undefined,
convert: (value: T) => U,
): void;
protected set_attr<T, U>(
attr: InputAttrsOfType<U>,
value?: T | Property<T>,
convert?: (value: T) => U,
): void {
if (value == undefined) return;
const input = this.input as any;
const cvt = convert ? convert : (v: T) => (v as any) as U;
if (is_any_property(value)) {
input[attr] = cvt(value.val);
this.disposable(value.observe(v => (input[attr] = cvt(v))));
} else {
input[attr] = cvt(value);
}
}
}
type InputAttrsOfType<T> = {
[K in keyof HTMLInputElement]: T extends HTMLInputElement[K] ? K : never;
}[keyof HTMLInputElement];

View File

@ -1,12 +1,12 @@
import { View } from "./View"; import { View } from "./View";
import { el } from "./dom"; import { create_element } from "./dom";
import { WritableProperty } from "../observable/WritableProperty"; import { WritableProperty } from "../observable/WritableProperty";
import "./Label.css"; import "./Label.css";
import { property } from "../observable"; import { property } from "../observable";
import { Property } from "../observable/Property"; import { Property } from "../observable/Property";
export class Label extends View { export class Label extends View {
readonly element = el<HTMLLabelElement>("label", { class: "core_Label" }); readonly element = create_element<HTMLLabelElement>("label", { class: "core_Label" });
set for(id: string) { set for(id: string) {
this.element.htmlFor = id; this.element.htmlFor = id;
@ -14,7 +14,7 @@ export class Label extends View {
readonly enabled: WritableProperty<boolean> = property(true); readonly enabled: WritableProperty<boolean> = property(true);
constructor(text: string | Property<string>) { constructor(text: string | Property<string>, options: { enabled?: boolean } = {}) {
super(); super();
if (typeof text === "string") { if (typeof text === "string") {
@ -33,5 +33,7 @@ export class Label extends View {
} }
}), }),
); );
if (options.enabled != undefined) this.enabled.val = options.enabled;
} }
} }

View File

@ -2,7 +2,7 @@ import { Label } from "./Label";
import { Control } from "./Control"; import { Control } from "./Control";
export abstract class LabelledControl extends Control { export abstract class LabelledControl extends Control {
abstract readonly preferred_label_position: "left" | "right"; abstract readonly preferred_label_position: "left" | "right" | "top" | "bottom";
private readonly _label_text: string; private readonly _label_text: string;
private _label?: Label; private _label?: Label;

View File

@ -1,29 +1,10 @@
import { View } from "./View"; import { View } from "./View";
import { el } from "./dom"; import { create_element } from "./dom";
import { Resizable } from "./Resizable"; import { Resizable } from "./Resizable";
import { ResizableView } from "./ResizableView"; import { ResizableView } from "./ResizableView";
export class LazyView extends ResizableView { export class LazyView extends ResizableView {
readonly element = el("div", { class: "core_LazyView" }); readonly element = create_element("div", { class: "core_LazyView" });
private _visible = false;
set visible(visible: boolean) {
if (this._visible !== visible) {
this._visible = visible;
this.element.hidden = !visible;
if (visible && !this.initialized) {
this.initialized = true;
this.create_view().then(view => {
this.view = this.disposable(view);
this.view.resize(this.width, this.height);
this.element.append(view.element);
});
}
}
}
private initialized = false; private initialized = false;
private view: View & Resizable | undefined; private view: View & Resizable | undefined;
@ -31,7 +12,21 @@ export class LazyView extends ResizableView {
constructor(private create_view: () => Promise<View & Resizable>) { constructor(private create_view: () => Promise<View & Resizable>) {
super(); super();
this.element.hidden = true; this.visible.val = false;
this.disposables(
this.visible.observe(visible => {
if (visible && !this.initialized) {
this.initialized = true;
this.create_view().then(view => {
this.view = this.disposable(view);
this.view.resize(this.width, this.height);
this.element.append(view.element);
});
}
}),
);
} }
resize(width: number, height: number): this { resize(width: number, height: number): this {

View File

@ -1,3 +1,3 @@
.core_NumberInput .core_NumberInput_inner { .core_NumberInput .core_NumberInput_inner {
text-align: right; padding-right: 1px;
} }

View File

@ -1,65 +1,43 @@
import "./NumberInput.css";
import "./Input.css";
import { el } from "./dom";
import { WritableProperty } from "../observable/WritableProperty";
import { property } from "../observable"; import { property } from "../observable";
import { LabelledControl } from "./LabelledControl"; import { Property } from "../observable/Property";
import { is_any_property, Property } from "../observable/Property"; import { Input } from "./Input";
import "./NumberInput.css"
export class NumberInput extends LabelledControl {
readonly element = el("span", { class: "core_NumberInput core_Input" });
readonly value: WritableProperty<number> = property(0);
export class NumberInput extends Input<number> {
readonly preferred_label_position = "left"; readonly preferred_label_position = "left";
private readonly input: HTMLInputElement = el("input", {
class: "core_NumberInput_inner core_Input_inner",
});
constructor( constructor(
value = 0, value: number = 0,
label?: string, options?: {
min: number | Property<number> = -Infinity, label?: string;
max: number | Property<number> = Infinity, min?: number | Property<number>;
step: number | Property<number> = 1, max?: number | Property<number>;
step?: number | Property<number>;
},
) { ) {
super(label); super(
property(value),
this.input.type = "number"; "core_NumberInput",
this.input.valueAsNumber = value; "number",
"core_NumberInput_inner",
this.set_prop("min", min); options && options.label,
this.set_prop("max", max);
this.set_prop("step", step);
this.input.onchange = () => (this.value.val = this.input.valueAsNumber);
this.element.append(this.input);
this.disposables(
this.value.observe(value => (this.input.valueAsNumber = value)),
this.enabled.observe(enabled => {
this.input.disabled = !enabled;
if (enabled) {
this.element.classList.remove("disabled");
} else {
this.element.classList.add("disabled");
}
}),
); );
this.element.style.width = "50px"; if (options) {
const { min, max, step } = options;
this.set_attr("min", min, String);
this.set_attr("max", max, String);
this.set_attr("step", step, String);
}
this.element.style.width = "54px";
} }
private set_prop<T>(prop: "min" | "max" | "step", value: T | Property<T>): void { protected get_input_value(): number {
if (is_any_property(value)) { return this.input.valueAsNumber;
this.input[prop] = String(value.val); }
this.disposable(value.observe(v => (this.input[prop] = String(v))));
} else { protected set_input_value(value: number): void {
this.input[prop] = String(value); this.input.valueAsNumber = value;
}
} }
} }

View File

@ -1,9 +1,9 @@
import { ResizableView } from "./ResizableView"; import { ResizableView } from "./ResizableView";
import { el } from "./dom"; import { create_element } from "./dom";
import { Renderer } from "../rendering/Renderer"; import { Renderer } from "../rendering/Renderer";
export class RendererView extends ResizableView { export class RendererView extends ResizableView {
readonly element = el("div"); readonly element = create_element("div");
constructor(private renderer: Renderer) { constructor(private renderer: Renderer) {
super(); super();

View File

@ -14,7 +14,7 @@
margin: 0 1px -1px 1px; margin: 0 1px -1px 1px;
background-color: hsl(0, 0%, 12%); background-color: hsl(0, 0%, 12%);
color: hsl(0, 0%, 75%); color: hsl(0, 0%, 75%);
font-size: 15px; font-size: 13px;
} }
.core_TabContainer_Tab:hover { .core_TabContainer_Tab:hover {

View File

@ -1,5 +1,5 @@
import { View } from "./View"; import { View } from "./View";
import { el } from "./dom"; import { create_element } from "./dom";
import { LazyView } from "./LazyView"; import { LazyView } from "./LazyView";
import { Resizable } from "./Resizable"; import { Resizable } from "./Resizable";
import { ResizableView } from "./ResizableView"; import { ResizableView } from "./ResizableView";
@ -16,19 +16,19 @@ type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyView };
const BAR_HEIGHT = 28; const BAR_HEIGHT = 28;
export class TabContainer extends ResizableView { export class TabContainer extends ResizableView {
readonly element = el("div", { class: "core_TabContainer" }); readonly element = create_element("div", { class: "core_TabContainer" });
private tabs: TabInfo[] = []; private tabs: TabInfo[] = [];
private bar_element = el("div", { class: "core_TabContainer_Bar" }); private bar_element = create_element("div", { class: "core_TabContainer_Bar" });
private panes_element = el("div", { class: "core_TabContainer_Panes" }); private panes_element = create_element("div", { class: "core_TabContainer_Panes" });
constructor(...tabs: Tab[]) { constructor(...tabs: Tab[]) {
super(); super();
this.bar_element.onclick = this.bar_click; this.bar_element.onmousedown = this.bar_mousedown;
for (const tab of tabs) { for (const tab of tabs) {
const tab_element = el("span", { const tab_element = create_element("span", {
class: "core_TabContainer_Tab", class: "core_TabContainer_Tab",
text: tab.title, text: tab.title,
data: { key: tab.key }, data: { key: tab.key },
@ -72,7 +72,7 @@ export class TabContainer extends ResizableView {
return this; return this;
} }
private bar_click = (e: MouseEvent) => { private bar_mousedown = (e: MouseEvent) => {
if (e.target instanceof HTMLElement) { if (e.target instanceof HTMLElement) {
const key = e.target.dataset["key"]; const key = e.target.dataset["key"];
if (key) this.activate(key); if (key) this.activate(key);
@ -89,7 +89,7 @@ export class TabContainer extends ResizableView {
tab.tab_element.classList.remove("active"); tab.tab_element.classList.remove("active");
} }
tab.lazy_view.visible = active; tab.lazy_view.visible.val = active;
} }
} }
} }

33
src/core/gui/TextArea.css Normal file
View File

@ -0,0 +1,33 @@
.core_TextArea {
box-sizing: border-box;
display: inline-block;
border: var(--input-border);
}
.core_TextArea .core_TextArea_inner {
box-sizing: border-box;
vertical-align: top;
padding: 3px;
border: var(--input-inner-border);
background-color: var(--input-bg-color);
color: var(--input-text-color);
outline: none;
font-size: 13px;
}
.core_TextArea:hover {
border-color: var(--input-border-hover);
}
.core_TextArea:focus-within {
border-color: var(--input-border-focus);
}
.core_TextArea.disabled {
border: var(--input-border-disabled);
}
.core_TextArea.disabled .core_TextArea_inner {
color: var(--input-text-color-disabled);
background-color: var(--input-bg-color-disabled);
}

46
src/core/gui/TextArea.ts Normal file
View File

@ -0,0 +1,46 @@
import { LabelledControl } from "./LabelledControl";
import { el } from "./dom";
import { property } from "../observable";
import { WritableProperty } from "../observable/WritableProperty";
import "./TextArea.css";
export class TextArea extends LabelledControl {
readonly element: HTMLElement = el.div({ class: "core_TextArea" });
readonly preferred_label_position = "left";
readonly value: WritableProperty<string>;
private readonly text_element: HTMLTextAreaElement = el.textarea({
class: "core_TextArea_inner",
});
constructor(
value = "",
options?: {
label?: string;
max_length?: number;
font_family?: string;
rows?: number;
cols?: number;
},
) {
super(options && options.label);
if (options) {
if (options.max_length != undefined) this.text_element.maxLength = options.max_length;
if (options.font_family != undefined)
this.text_element.style.fontFamily = options.font_family;
if (options.rows != undefined) this.text_element.rows = options.rows;
if (options.cols != undefined) this.text_element.cols = options.cols;
}
this.value = property(value);
this.text_element.onchange = () => (this.value.val = this.text_element.value);
this.disposables(this.value.observe(value => (this.text_element.value = value)));
this.element.append(this.text_element);
}
}

36
src/core/gui/TextInput.ts Normal file
View File

@ -0,0 +1,36 @@
import { Input } from "./Input";
import { Property } from "../observable/Property";
import { property } from "../observable";
export class TextInput extends Input<string> {
readonly preferred_label_position = "left";
constructor(
value = "",
options?: {
label?: string;
max_length?: number | Property<number>;
},
) {
super(
property(value),
"core_TextInput",
"text",
"core_TextInput_inner",
options && options.label,
);
if (options) {
const { max_length } = options;
this.set_attr("maxLength", max_length);
}
}
protected get_input_value(): string {
return this.input.value;
}
protected set_input_value(value: string): void {
this.input.value = value;
}
}

View File

@ -20,3 +20,7 @@
.core_ToolBar > .core_ToolBar_group > * { .core_ToolBar > .core_ToolBar_group > * {
margin: 0 2px; margin: 0 2px;
} }
.core_ToolBar .core_Input {
height: 26px;
}

View File

@ -1,10 +1,10 @@
import { View } from "./View"; import { View } from "./View";
import { el } from "./dom"; import { create_element } from "./dom";
import "./ToolBar.css"; import "./ToolBar.css";
import { LabelledControl } from "./LabelledControl"; import { LabelledControl } from "./LabelledControl";
export class ToolBar extends View { export class ToolBar extends View {
readonly element = el("div", { class: "core_ToolBar" }); readonly element = create_element("div", { class: "core_ToolBar" });
readonly height = 33; readonly height = 33;
constructor(...children: View[]) { constructor(...children: View[]) {
@ -14,9 +14,12 @@ export class ToolBar extends View {
for (const child of children) { for (const child of children) {
if (child instanceof LabelledControl) { if (child instanceof LabelledControl) {
const group = el("div", { class: "core_ToolBar_group" }); const group = create_element("div", { class: "core_ToolBar_group" });
if (child.preferred_label_position === "left") { if (
child.preferred_label_position === "left" ||
child.preferred_label_position === "top"
) {
group.append(child.label.element, child.element); group.append(child.label.element, child.element);
} else { } else {
group.append(child.element, child.label.element); group.append(child.element, child.label.element);

View File

@ -2,6 +2,8 @@ import { Disposable } from "../observable/Disposable";
import { Disposer } from "../observable/Disposer"; import { Disposer } from "../observable/Disposer";
import { Observable } from "../observable/Observable"; import { Observable } from "../observable/Observable";
import { bind_hidden } from "./dom"; import { bind_hidden } from "./dom";
import { WritableProperty } from "../observable/WritableProperty";
import { property } from "../observable";
export abstract class View implements Disposable { export abstract class View implements Disposable {
abstract readonly element: HTMLElement; abstract readonly element: HTMLElement;
@ -14,8 +16,14 @@ export abstract class View implements Disposable {
this.element.id = id; this.element.id = id;
} }
readonly visible: WritableProperty<boolean> = property(true);
private disposer = new Disposer(); private disposer = new Disposer();
constructor() {
this.disposables(this.visible.observe(visible => (this.element.hidden = !visible)));
}
dispose(): void { dispose(): void {
this.element.remove(); this.element.remove();
this.disposer.dispose(); this.disposer.dispose();

View File

@ -2,16 +2,41 @@ import { Disposable } from "../observable/Disposable";
import { Observable } from "../observable/Observable"; import { Observable } from "../observable/Observable";
import { is_property } from "../observable/Property"; import { is_property } from "../observable/Property";
export function el<T extends HTMLElement>( export const el = {
div: (attributes?: {}, ...children: HTMLElement[]): HTMLDivElement =>
create_element("div", attributes, ...children),
table: (attributes?: {}, ...children: HTMLElement[]): HTMLTableElement =>
create_element("table", attributes, ...children),
tr: (attributes?: {}, ...children: HTMLElement[]): HTMLTableRowElement =>
create_element("tr", attributes, ...children),
th: (
attributes?: { text?: string; col_span?: number },
...children: HTMLElement[]
): HTMLTableHeaderCellElement => create_element("th", attributes, ...children),
td: (
attributes?: { text?: string; col_span?: number },
...children: HTMLElement[]
): HTMLTableCellElement => create_element("td", attributes, ...children),
textarea: (attributes?: {}, ...children: HTMLElement[]): HTMLTextAreaElement =>
create_element("textarea", attributes, ...children),
};
export function create_element<T extends HTMLElement>(
tag_name: string, tag_name: string,
attributes?: { attributes?: {
class?: string; class?: string;
text?: string ; text?: string;
data?: { [key: string]: string }; data?: { [key: string]: string };
col_span?: number;
}, },
...children: HTMLElement[] ...children: HTMLElement[]
): T { ): T {
const element = document.createElement(tag_name) as T; const element = document.createElement(tag_name) as HTMLTableCellElement;
if (attributes) { if (attributes) {
if (attributes.class) element.className = attributes.class; if (attributes.class) element.className = attributes.class;
@ -22,11 +47,13 @@ export function el<T extends HTMLElement>(
element.dataset[key] = val; element.dataset[key] = val;
} }
} }
if (attributes.col_span) element.colSpan = attributes.col_span;
} }
element.append(...children); element.append(...children);
return element; return (element as HTMLElement) as T;
} }
export function bind_hidden(element: HTMLElement, observable: Observable<boolean>): Disposable { export function bind_hidden(element: HTMLElement, observable: Observable<boolean>): Disposable {

View File

@ -17,7 +17,7 @@
margin: 0 1px -1px 1px; margin: 0 1px -1px 1px;
background-color: hsl(0, 0%, 12%); background-color: hsl(0, 0%, 12%);
color: hsl(0, 0%, 75%); color: hsl(0, 0%, 75%);
font-size: 15px; font-size: 13px;
} }
#root .lm_tab:hover { #root .lm_tab:hover {

View File

@ -1,11 +1,28 @@
:root { :root {
/* Basic view variables */
--bg-color: hsl(0, 0%, 15%); --bg-color: hsl(0, 0%, 15%);
--text-color: hsl(0, 0%, 80%); --text-color: hsl(0, 0%, 80%);
--text-color-disabled: hsl(0, 0%, 55%); --text-color-disabled: hsl(0, 0%, 55%);
--border-color: hsl(0, 0%, 25%); --border-color: hsl(0, 0%, 25%);
/* Scrollbars */
--scrollbar-color: hsl(0, 0%, 13%); --scrollbar-color: hsl(0, 0%, 13%);
--scrollbar-thumb-color: hsl(0, 0%, 17%); --scrollbar-thumb-color: hsl(0, 0%, 17%);
/* Inputs */
--input-bg-color: hsl(0, 0%, 12%);
--input-bg-color-disabled: hsl(0, 0%, 15%);
--input-text-color: hsl(0, 0%, 75%);
--input-text-color-disabled: var(--text-color-disabled);
--input-border: solid 1px hsl(0, 0%, 25%);
--input-border-hover: hsl(0, 0%, 35%);
--input-border-focus: hsl(0, 0%, 45%);
--input-border-disabled: solid 1px hsl(0, 0%, 20%);
--input-inner-border: solid 1px hsl(0, 0%, 5%);
} }
* { * {
@ -33,9 +50,15 @@ body {
user-select: none; user-select: none;
overflow: hidden; overflow: hidden;
margin: 0; margin: 0;
font-size: 15px; font-size: 13px;
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica,
Arial, sans-serif;
background-color: var(--bg-color); background-color: var(--bg-color);
color: var(--text-color); color: var(--text-color);
} }
* {
font-family: Verdana, Geneva, sans-serif;
}
#root *[hidden] {
display: none;
}

View File

@ -15,7 +15,7 @@ export class DependentProperty<T> extends AbstractMinimalProperty<T> implements
private _val?: T; private _val?: T;
get val(): T { get val(): T {
if (this.dependency_disposables) { if (this.dependency_disposables.length) {
return this._val as T; return this._val as T;
} else { } else {
return this.f(); return this.f();

View File

@ -15,5 +15,5 @@ export function is_property<T>(observable: Observable<T>): observable is Propert
} }
export function is_any_property(observable: any): observable is Property<any> { export function is_any_property(observable: any): observable is Property<any> {
return (observable as any).is_property; return observable && (observable as any).is_property;
} }

View File

@ -1,6 +1,6 @@
import { ApplicationView } from "./application/gui/ApplicationView"; import { ApplicationView } from "./application/gui/ApplicationView";
import { Disposable } from "./core/observable/Disposable"; import { Disposable } from "./core/observable/Disposable";
import "./index.css"; import "./core/gui/index.css";
import { throttle } from "lodash"; import { throttle } from "lodash";
import Logger from "js-logger"; import Logger from "js-logger";

View File

@ -1,6 +1,6 @@
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableView } from "../../core/gui/ResizableView";
import { el } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
export class NpcCountsView extends ResizableView { export class NpcCountsView extends ResizableView {
readonly element = el("div"); readonly element = create_element("div");
} }

View File

@ -0,0 +1,33 @@
.quest_editor_QuesInfoView {
box-sizing: border-box;
padding: 3px;
overflow: auto;
}
.quest_editor_QuesInfoView table {
width: 100%;
}
.quest_editor_QuesInfoView th {
text-align: left;
}
.quest_editor_QuesInfoView .core_TextInput {
width: 100%;
}
.quest_editor_QuesInfoView .core_TextArea {
width: 100%;
}
.quest_editor_QuesInfoView textarea {
width: 100%;
}
.quest_editor_QuesInfoView_no_quest {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}

View File

@ -2,36 +2,78 @@ import { ResizableView } from "../../core/gui/ResizableView";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { quest_editor_store } from "../stores/QuestEditorStore"; import { quest_editor_store } from "../stores/QuestEditorStore";
import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { NumberInput } from "../../core/gui/NumberInput";
import { Disposer } from "../../core/observable/Disposer";
import { TextInput } from "../../core/gui/TextInput";
import { TextArea } from "../../core/gui/TextArea";
import "./QuesInfoView.css";
import { Label } from "../../core/gui/Label";
export class QuesInfoView extends ResizableView { export class QuesInfoView extends ResizableView {
readonly element = el("div", { class: "quest_editor_QuesInfoView" }); readonly element = el.div({ class: "quest_editor_QuesInfoView" });
private readonly table_element = el("table"); private readonly table_element = el.table();
private readonly episode_element: HTMLElement; private readonly episode_element: HTMLElement;
private readonly id_element: HTMLElement; private readonly id_input = this.disposable(new NumberInput());
private readonly name_element: HTMLElement; private readonly name_input = this.disposable(new TextInput());
private readonly short_description_input = this.disposable(
new TextArea("", {
max_length: 128,
font_family: '"Courier New", monospace',
cols: 25,
rows: 5,
}),
);
private readonly long_description_input = this.disposable(
new TextArea("", {
max_length: 288,
font_family: '"Courier New", monospace',
cols: 25,
rows: 10,
}),
);
private readonly no_quest_element = el.div({ class: "quest_editor_QuesInfoView_no_quest" });
private readonly no_quest_label = this.disposable(
new Label("No quest loaded.", { enabled: false }),
);
private readonly quest_disposer = this.disposable(new Disposer());
constructor() { constructor() {
super(); super();
const quest = quest_editor_store.current_quest; const quest = quest_editor_store.current_quest;
this.bind_hidden(this.table_element, quest.map(q => q == undefined)); this.no_quest_element.append(this.no_quest_label.element);
this.bind_hidden(this.no_quest_element, quest.map(q => q != undefined));
this.table_element.append( this.table_element.append(
el("tr", {}, el("th", { text: "Episode:" }), (this.episode_element = el("td"))), el.tr({}, el.th({ text: "Episode:" }), (this.episode_element = el.td())),
el("tr", {}, el("th", { text: "ID:" }), (this.id_element = el("td"))), el.tr({}, el.th({ text: "ID:" }), el.td({}, this.id_input.element)),
el("tr", {}, el("th", { text: "Name:" }), (this.name_element = el("td"))), el.tr({}, el.th({ text: "Name:" }), el.td({}, this.name_input.element)),
el.tr({}, el.th({ text: "Short description:", col_span: 2 })),
el.tr({}, el.td({ col_span: 2 }, this.short_description_input.element)),
el.tr({}, el.th({ text: "Long description:", col_span: 2 })),
el.tr({}, el.td({ col_span: 2 }, this.long_description_input.element)),
); );
this.bind_hidden(this.table_element, quest.map(q => q == undefined));
this.element.append(this.table_element); this.element.append(this.table_element, this.no_quest_element);
this.disposables( this.disposables(
quest.observe(q => { quest.observe(q => {
this.quest_disposer.dispose();
this.episode_element.textContent = q ? Episode[q.episode] : "";
if (q) { if (q) {
this.episode_element.textContent = Episode[q.episode]; this.quest_disposer.add_all(
this.id_element.textContent = q.id.val.toString(); this.id_input.value.bind_bi(q.id),
this.name_element.textContent = q.name.val; this.name_input.value.bind_bi(q.name),
this.short_description_input.value.bind_bi(q.short_description),
this.long_description_input.value.bind_bi(q.long_description),
);
} }
}), }),
); );

View File

@ -1,5 +1,5 @@
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableView } from "../../core/gui/ResizableView";
import { el } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import { ToolBarView } from "./ToolBarView"; import { ToolBarView } from "./ToolBarView";
import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout"; import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout";
import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister"; import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister";
@ -89,11 +89,11 @@ const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [
]; ];
export class QuestEditorView extends ResizableView { export class QuestEditorView extends ResizableView {
readonly element = el("div", { class: "quest_editor_QuestEditorView" }); readonly element = create_element("div", { class: "quest_editor_QuestEditorView" });
private readonly tool_bar_view = this.disposable(new ToolBarView()); private readonly tool_bar_view = this.disposable(new ToolBarView());
private readonly layout_element = el("div", { class: "quest_editor_gl_container" }); private readonly layout_element = create_element("div", { class: "quest_editor_gl_container" });
private readonly layout: Promise<GoldenLayout>; private readonly layout: Promise<GoldenLayout>;
constructor() { constructor() {

View File

@ -6,14 +6,20 @@ import { parse_quest } from "../../core/data_formats/parsing/quest";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness"; import { Endianness } from "../../core/data_formats/Endianness";
import { SimpleUndo, UndoStack } from "../../old/core/undo"; import { SimpleUndo, UndoStack } from "../../old/core/undo";
import { WritableProperty } from "../../core/observable/WritableProperty";
import Logger = require("js-logger"); import Logger = require("js-logger");
const logger = Logger.get("quest_editor/gui/QuestEditorStore"); const logger = Logger.get("quest_editor/gui/QuestEditorStore");
export class QuestEditorStore { export class QuestEditorStore {
readonly debug: WritableProperty<boolean> = property(false);
readonly undo = new UndoStack(); readonly undo = new UndoStack();
readonly script_undo = new SimpleUndo("Text edits", () => {}, () => {}); readonly script_undo = new SimpleUndo("Text edits", () => {}, () => {});
private readonly _current_quest_filename = property<string | undefined>(undefined);
readonly current_quest_filename: Property<string | undefined> = this._current_quest_filename;
private readonly _current_quest = property<ObservableQuest | undefined>(undefined); private readonly _current_quest = property<ObservableQuest | undefined>(undefined);
readonly current_quest: Property<ObservableQuest | undefined> = this._current_quest; readonly current_quest: Property<ObservableQuest | undefined> = this._current_quest;
@ -74,7 +80,7 @@ export class QuestEditorStore {
}; };
private set_quest(quest?: ObservableQuest, filename?: string): void { private set_quest(quest?: ObservableQuest, filename?: string): void {
// this.current_quest_filename = filename; this._current_quest_filename.val = filename;
this.undo.reset(); this.undo.reset();
this.script_undo.reset(); this.script_undo.reset();

View File

@ -1,4 +1,4 @@
import { el } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableView } from "../../core/gui/ResizableView";
import { ToolBar } from "../../core/gui/ToolBar"; import { ToolBar } from "../../core/gui/ToolBar";
import "./ModelView.css"; import "./ModelView.css";
@ -18,10 +18,10 @@ const MODEL_LIST_WIDTH = 100;
const ANIMATION_LIST_WIDTH = 130; const ANIMATION_LIST_WIDTH = 130;
export class ModelView extends ResizableView { export class ModelView extends ResizableView {
readonly element = el("div", { class: "viewer_ModelView" }); readonly element = create_element("div", { class: "viewer_ModelView" });
private tool_bar_view = this.disposable(new ToolBarView()); private tool_bar_view = this.disposable(new ToolBarView());
private container_element = el("div", { class: "viewer_ModelView_container" }); private container_element = create_element("div", { class: "viewer_ModelView_container" });
private model_list_view = this.disposable( private model_list_view = this.disposable(
new ModelSelectListView(model_store.models, model_store.current_model), new ModelSelectListView(model_store.models, model_store.current_model),
); );
@ -78,20 +78,18 @@ class ToolBarView extends View {
private readonly open_file_button = new FileButton("Open file...", ".nj, .njm, .xj, .xvm"); private readonly open_file_button = new FileButton("Open file...", ".nj, .njm, .xj, .xvm");
private readonly skeleton_checkbox = new CheckBox(false, "Show skeleton"); private readonly skeleton_checkbox = new CheckBox(false, "Show skeleton");
private readonly play_animation_checkbox = new CheckBox(true, "Play animation"); private readonly play_animation_checkbox = new CheckBox(true, "Play animation");
private readonly animation_frame_rate_input = new NumberInput( private readonly animation_frame_rate_input = new NumberInput(PSO_FRAME_RATE, {
PSO_FRAME_RATE, label: "Frame rate:",
"Frame rate:", min: 1,
1, max: 240,
240, step: 1,
1, });
); private readonly animation_frame_input = new NumberInput(1, {
private readonly animation_frame_input = new NumberInput( label: "Frame:",
1, min: 1,
"Frame:", max: model_store.animation_frame_count,
1, step: 1,
model_store.animation_frame_count, });
1,
);
private readonly animation_frame_count_label = new Label( private readonly animation_frame_count_label = new Label(
model_store.animation_frame_count.map(count => `/ ${count}`), model_store.animation_frame_count.map(count => `/ ${count}`),
); );
@ -147,7 +145,7 @@ class ToolBarView extends View {
} }
class ModelSelectListView<T extends { name: string }> extends ResizableView { class ModelSelectListView<T extends { name: string }> extends ResizableView {
element = el("ul", { class: "viewer_ModelSelectListView" }); element = create_element("ul", { class: "viewer_ModelSelectListView" });
set borders(borders: boolean) { set borders(borders: boolean) {
if (borders) { if (borders) {
@ -169,7 +167,7 @@ class ModelSelectListView<T extends { name: string }> extends ResizableView {
models.forEach((model, index) => { models.forEach((model, index) => {
this.element.append( this.element.append(
el("li", { text: model.name, data: { index: index.toString() } }), create_element("li", { text: model.name, data: { index: index.toString() } }),
); );
}); });

View File

@ -1,4 +1,4 @@
import { el } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableView } from "../../core/gui/ResizableView";
import { FileButton } from "../../core/gui/FileButton"; import { FileButton } from "../../core/gui/FileButton";
import { ToolBar } from "../../core/gui/ToolBar"; import { ToolBar } from "../../core/gui/ToolBar";
@ -8,7 +8,7 @@ import { TextureRenderer } from "../rendering/TextureRenderer";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { gui_store, GuiTool } from "../../core/stores/GuiStore";
export class TextureView extends ResizableView { export class TextureView extends ResizableView {
readonly element = el("div", { class: "viewer_TextureView" }); readonly element = create_element("div", { class: "viewer_TextureView" });
private readonly open_file_button = new FileButton("Open file...", ".xvm"); private readonly open_file_button = new FileButton("Open file...", ".xvm");

View File

@ -36,6 +36,7 @@ export class ModelRenderer extends Renderer implements Disposable {
clip: AnimationClip; clip: AnimationClip;
action: AnimationAction; action: AnimationAction;
}; };
private update_animation_time = true;
constructor() { constructor() {
super(new PerspectiveCamera(75, 1, 1, 200)); super(new PerspectiveCamera(75, 1, 1, 200));
@ -201,7 +202,11 @@ export class ModelRenderer extends Renderer implements Disposable {
const frame_count = nj_motion.frame_count; const frame_count = nj_motion.frame_count;
if (frame > frame_count) frame = 1; if (frame > frame_count) frame = 1;
if (frame < 1) frame = frame_count; if (frame < 1) frame = frame_count;
this.animation.action.time = (frame - 1) / PSO_FRAME_RATE;
if (this.update_animation_time) {
this.animation.action.time = (frame - 1) / PSO_FRAME_RATE;
}
this.schedule_render(); this.schedule_render();
} }
}; };
@ -209,7 +214,9 @@ export class ModelRenderer extends Renderer implements Disposable {
private update_animation_frame(): void { private update_animation_frame(): void {
if (this.animation && !this.animation.action.paused) { if (this.animation && !this.animation.action.paused) {
const time = this.animation.action.time; const time = this.animation.action.time;
this.update_animation_time = false;
model_store.animation_frame.val = time * PSO_FRAME_RATE + 1; model_store.animation_frame.val = time * PSO_FRAME_RATE + 1;
this.update_animation_time = true;
} }
} }
} }