mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
ListProperties can now emit update events.
This commit is contained in:
parent
1039049fda
commit
62db02e278
@ -22,7 +22,7 @@
|
||||
box-sizing: border-box;
|
||||
background-color: var(--control-bg-color);
|
||||
height: 24px;
|
||||
padding: 3px 8px;
|
||||
padding: 3px 5px;
|
||||
border: var(--control-inner-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -45,6 +45,11 @@
|
||||
color: hsl(0, 0%, 55%);
|
||||
}
|
||||
|
||||
.core_Button_inner > * {
|
||||
display: inline-block;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.core_Button_center {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
@ -59,11 +64,3 @@
|
||||
align-content: center;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.core_Button_left {
|
||||
padding: 0 6px 0 0;
|
||||
}
|
||||
|
||||
.core_Button_right {
|
||||
padding: 0 0 0 6px;
|
||||
}
|
||||
|
@ -31,11 +31,12 @@ export class Button extends Control<HTMLButtonElement> {
|
||||
|
||||
super(el.button({ class: "core_Button" }, inner_element), options);
|
||||
|
||||
this.center_element = el.span({ class: "core_Button_center" });
|
||||
|
||||
if (options && options.icon_left != undefined) {
|
||||
inner_element.append(el.span({ class: "core_Button_left" }, icon(options.icon_left)));
|
||||
}
|
||||
|
||||
this.center_element = el.span({ class: "core_Button_center" });
|
||||
inner_element.append(this.center_element);
|
||||
|
||||
if (options && options.icon_right != undefined) {
|
||||
@ -71,5 +72,6 @@ export class Button extends Control<HTMLButtonElement> {
|
||||
|
||||
protected set_text(text: string): void {
|
||||
this.center_element.textContent = text;
|
||||
this.center_element.hidden = text === "";
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export class FileButton extends Control<HTMLElement> {
|
||||
);
|
||||
}
|
||||
|
||||
inner_element.append(el.span({ text }));
|
||||
inner_element.append(el.span({ class: "core_Button_center", text }));
|
||||
|
||||
this.element.append(inner_element, this.input);
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { Property } from "./property/Property";
|
||||
import { DependentProperty } from "./property/DependentProperty";
|
||||
import { WritableListProperty } from "./property/list/WritableListProperty";
|
||||
import { SimpleWritableListProperty } from "./property/list/SimpleWritableListProperty";
|
||||
import { Observable } from "./Observable";
|
||||
|
||||
export function emitter<E>(): Emitter<E> {
|
||||
return new SimpleEmitter();
|
||||
@ -15,8 +16,11 @@ export function property<T>(value: T): WritableProperty<T> {
|
||||
return new SimpleProperty(value);
|
||||
}
|
||||
|
||||
export function list_property<T>(...values: T[]): WritableListProperty<T> {
|
||||
return new SimpleWritableListProperty(...values);
|
||||
export function list_property<T>(
|
||||
extract_observables?: (element: T) => Observable<any>[],
|
||||
...elements: T[]
|
||||
): WritableListProperty<T> {
|
||||
return new SimpleWritableListProperty(extract_observables, ...elements);
|
||||
}
|
||||
|
||||
export function add(left: Property<number>, right: number): Property<number> {
|
||||
|
@ -17,13 +17,13 @@ export abstract class AbstractMinimalProperty<T> implements Property<T> {
|
||||
|
||||
observe(
|
||||
observer: (change: PropertyChangeEvent<T>) => void,
|
||||
options: { call_now?: boolean } = {},
|
||||
options?: { call_now?: boolean },
|
||||
): Disposable {
|
||||
if (!this.observers.includes(observer)) {
|
||||
this.observers.push(observer);
|
||||
}
|
||||
|
||||
if (options.call_now) {
|
||||
if (options && options.call_now) {
|
||||
this.call_observer(observer, this.val);
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ export type ListReplacement<T> = {
|
||||
|
||||
export type ListUpdate<T> = {
|
||||
readonly type: ListChangeType.Update;
|
||||
readonly update: T[];
|
||||
readonly updated: T[];
|
||||
readonly index: number;
|
||||
};
|
||||
|
||||
@ -48,5 +48,8 @@ export interface ListProperty<T> extends Property<T[]> {
|
||||
|
||||
get(index: number): T;
|
||||
|
||||
observe_list(observer: (change: ListPropertyChangeEvent<T>) => void): Disposable;
|
||||
observe_list(
|
||||
observer: (change: ListPropertyChangeEvent<T>) => void,
|
||||
options?: { call_now?: boolean },
|
||||
): Disposable;
|
||||
}
|
||||
|
@ -12,49 +12,80 @@ const logger = Logger.get("core/observable/property/list/SimpleWritableListPrope
|
||||
|
||||
export class SimpleWritableListProperty<T> extends AbstractProperty<T[]>
|
||||
implements WritableListProperty<T> {
|
||||
readonly length: Property<number>;
|
||||
readonly length: Property<number>; // TODO: update length
|
||||
|
||||
get val(): T[] {
|
||||
return this.get_val();
|
||||
}
|
||||
|
||||
set val(values: T[]) {
|
||||
this.set_val(values);
|
||||
set val(elements: T[]) {
|
||||
this.set_val(elements);
|
||||
}
|
||||
|
||||
get_val(): T[] {
|
||||
return this.values;
|
||||
return this.elements;
|
||||
}
|
||||
|
||||
set_val(values: T[]): T[] {
|
||||
const removed = this.values.splice(0, this.values.length, ...values);
|
||||
set_val(elements: T[]): T[] {
|
||||
const removed = this.elements.splice(0, this.elements.length, ...elements);
|
||||
this.emit_list({
|
||||
type: ListChangeType.Replacement,
|
||||
removed,
|
||||
inserted: values,
|
||||
inserted: elements,
|
||||
from: 0,
|
||||
removed_to: removed.length,
|
||||
inserted_to: values.length,
|
||||
inserted_to: elements.length,
|
||||
});
|
||||
return removed;
|
||||
}
|
||||
|
||||
private readonly _length = property(0);
|
||||
private readonly values: T[];
|
||||
private readonly elements: T[];
|
||||
private readonly extract_observables?: (element: T) => Observable<any>[];
|
||||
/**
|
||||
* Internal observers which observe observables related to this list's elements so that their
|
||||
* changes can be propagated via update events.
|
||||
*/
|
||||
private readonly element_observers: { index: number; disposables: Disposable[] }[] = [];
|
||||
/**
|
||||
* External observers which are observing this list.
|
||||
*/
|
||||
private readonly list_observers: ((change: ListPropertyChangeEvent<T>) => void)[] = [];
|
||||
|
||||
constructor(...values: T[]) {
|
||||
/**
|
||||
* @param extract_observables - Extractor function called on each element in this list. Changes
|
||||
* to the returned observables will be propagated via update events.
|
||||
* @param elements - Initial elements of this list.
|
||||
*/
|
||||
constructor(extract_observables?: (element: T) => Observable<any>[], ...elements: T[]) {
|
||||
super();
|
||||
|
||||
this.length = this._length;
|
||||
this.values = values;
|
||||
this.elements = elements;
|
||||
this.extract_observables = extract_observables;
|
||||
}
|
||||
|
||||
observe_list(observer: (change: ListPropertyChangeEvent<T>) => void): Disposable {
|
||||
observe_list(
|
||||
observer: (change: ListPropertyChangeEvent<T>) => void,
|
||||
options?: { call_now?: true },
|
||||
): Disposable {
|
||||
if (this.element_observers.length === 0 && this.extract_observables) {
|
||||
this.replace_element_observers(this.elements, 0, Infinity);
|
||||
}
|
||||
|
||||
if (!this.list_observers.includes(observer)) {
|
||||
this.list_observers.push(observer);
|
||||
}
|
||||
|
||||
if (options && options.call_now) {
|
||||
this.call_list_observer(observer, {
|
||||
type: ListChangeType.Insertion,
|
||||
inserted: this.elements,
|
||||
from: 0,
|
||||
to: this.elements.length,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
const index = this.list_observers.indexOf(observer);
|
||||
@ -62,6 +93,16 @@ export class SimpleWritableListProperty<T> extends AbstractProperty<T[]>
|
||||
if (index !== -1) {
|
||||
this.list_observers.splice(index, 1);
|
||||
}
|
||||
|
||||
if (this.list_observers.length === 0) {
|
||||
for (const { disposables } of this.element_observers) {
|
||||
for (const disposable of disposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
this.element_observers.splice(0, Infinity);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -74,21 +115,21 @@ export class SimpleWritableListProperty<T> extends AbstractProperty<T[]>
|
||||
/* TODO */ throw new Error("not implemented");
|
||||
}
|
||||
|
||||
update(f: (value: T[]) => T[]): void {
|
||||
this.splice(0, this.values.length, ...f(this.values));
|
||||
update(f: (element: T[]) => T[]): void {
|
||||
this.splice(0, this.elements.length, ...f(this.elements));
|
||||
}
|
||||
|
||||
get(index: number): T {
|
||||
return this.values[index];
|
||||
return this.elements[index];
|
||||
}
|
||||
|
||||
set(index: number, value: T): void {
|
||||
const removed = [this.values[index]];
|
||||
this.values[index] = value;
|
||||
set(index: number, element: T): void {
|
||||
const removed = [this.elements[index]];
|
||||
this.elements[index] = element;
|
||||
this.emit_list({
|
||||
type: ListChangeType.Replacement,
|
||||
removed,
|
||||
inserted: [value],
|
||||
inserted: [element],
|
||||
from: index,
|
||||
removed_to: index + 1,
|
||||
inserted_to: index + 1,
|
||||
@ -96,7 +137,7 @@ export class SimpleWritableListProperty<T> extends AbstractProperty<T[]>
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
const removed = this.values.splice(0, this.values.length);
|
||||
const removed = this.elements.splice(0, this.elements.length);
|
||||
this.emit_list({
|
||||
type: ListChangeType.Replacement,
|
||||
removed,
|
||||
@ -111,9 +152,9 @@ export class SimpleWritableListProperty<T> extends AbstractProperty<T[]>
|
||||
let removed: T[];
|
||||
|
||||
if (delete_count == undefined) {
|
||||
removed = this.values.splice(index);
|
||||
removed = this.elements.splice(index);
|
||||
} else {
|
||||
removed = this.values.splice(index, delete_count, ...items);
|
||||
removed = this.elements.splice(index, delete_count, ...items);
|
||||
}
|
||||
|
||||
this.emit_list({
|
||||
@ -129,14 +170,76 @@ export class SimpleWritableListProperty<T> extends AbstractProperty<T[]>
|
||||
}
|
||||
|
||||
protected emit_list(change: ListPropertyChangeEvent<T>): void {
|
||||
for (const observer of this.list_observers) {
|
||||
try {
|
||||
observer(change);
|
||||
} catch (e) {
|
||||
logger.error("Observer threw error.", e);
|
||||
if (this.list_observers.length && this.extract_observables) {
|
||||
switch (change.type) {
|
||||
case ListChangeType.Insertion:
|
||||
this.replace_element_observers(change.inserted, change.from, 0);
|
||||
break;
|
||||
|
||||
case ListChangeType.Removal:
|
||||
this.replace_element_observers([], change.from, change.removed.length);
|
||||
break;
|
||||
|
||||
case ListChangeType.Replacement:
|
||||
this.replace_element_observers(
|
||||
change.inserted,
|
||||
change.from,
|
||||
change.removed.length,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(this.values);
|
||||
for (const observer of this.list_observers) {
|
||||
this.call_list_observer(observer, change);
|
||||
}
|
||||
|
||||
this.emit(this.elements);
|
||||
}
|
||||
|
||||
private call_list_observer(
|
||||
observer: (change: ListPropertyChangeEvent<T>) => void,
|
||||
change: ListPropertyChangeEvent<T>,
|
||||
): void {
|
||||
try {
|
||||
observer(change);
|
||||
} catch (e) {
|
||||
logger.error("Observer threw error.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private replace_element_observers(new_elements: T[], from: number, amount: number): void {
|
||||
let index = from;
|
||||
|
||||
const removed = this.element_observers.splice(
|
||||
from,
|
||||
amount,
|
||||
...new_elements.map(element => {
|
||||
const obj = {
|
||||
index,
|
||||
disposables: this.extract_observables!(element).map(observable =>
|
||||
observable.observe(() => {
|
||||
this.emit_list({
|
||||
type: ListChangeType.Update,
|
||||
updated: [element],
|
||||
index: obj.index,
|
||||
});
|
||||
}),
|
||||
),
|
||||
};
|
||||
index++;
|
||||
return obj;
|
||||
}),
|
||||
);
|
||||
|
||||
for (const { disposables } of removed) {
|
||||
for (const disposable of disposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
while (index < this.element_observers.length) {
|
||||
this.element_observers[index].index += index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5
src/hunt_optimizer/gui/OptimizerView.css
Normal file
5
src/hunt_optimizer/gui/OptimizerView.css
Normal file
@ -0,0 +1,5 @@
|
||||
.hunt_optimizer_OptimizerView {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { ResizableWidget } from "../../core/gui/ResizableWidget";
|
||||
import { el } from "../../core/gui/dom";
|
||||
import { WantedItemsView } from "./WantedItemsView";
|
||||
import "./OptimizerView.css";
|
||||
|
||||
export class OptimizerView extends ResizableWidget {
|
||||
private readonly wanted_items_view: WantedItemsView;
|
||||
@ -11,12 +12,4 @@ export class OptimizerView extends ResizableWidget {
|
||||
this.wanted_items_view = this.disposable(new WantedItemsView());
|
||||
this.element.append(this.wanted_items_view.element);
|
||||
}
|
||||
|
||||
resize(width: number, height: number): this {
|
||||
super.resize(width, height);
|
||||
|
||||
this.wanted_items_view.resize(Math.min(200, width), height);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -2,4 +2,16 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hunt_optimizer_WantedItemsView .hunt_optimizer_WantedItemsView_table_wrapper {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
.hunt_optimizer_WantedItemsView .hunt_optimizer_WantedItemsView_table_wrapper table {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -1,36 +1,103 @@
|
||||
import { ResizableWidget } from "../../core/gui/ResizableWidget";
|
||||
import { el, Icon } from "../../core/gui/dom";
|
||||
import "./WantedItemsView.css";
|
||||
import { hunt_optimizer_store } from "../stores/HuntOptimizerStore";
|
||||
import { Button } from "../../core/gui/Button";
|
||||
import { Disposer } from "../../core/observable/Disposer";
|
||||
import { Widget } from "../../core/gui/Widget";
|
||||
import {
|
||||
ListChangeType,
|
||||
ListPropertyChangeEvent,
|
||||
} from "../../core/observable/property/list/ListProperty";
|
||||
import { WantedItemModel } from "../model";
|
||||
import { NumberInput } from "../../core/gui/NumberInput";
|
||||
|
||||
export class WantedItemsView extends ResizableWidget {
|
||||
export class WantedItemsView extends Widget {
|
||||
private readonly tbody_element = el.tbody();
|
||||
private readonly table_disposer = this.disposable(new Disposer());
|
||||
|
||||
constructor() {
|
||||
super(el.div({ class: "hunt_optimizer_WantedItemsView" }));
|
||||
|
||||
this.element.append(el.h2({ text: "Wanted Items" }), el.table({}, this.tbody_element));
|
||||
this.element.append(
|
||||
el.h2({ text: "Wanted Items" }),
|
||||
el.div(
|
||||
{ class: "hunt_optimizer_WantedItemsView_table_wrapper" },
|
||||
el.table({}, this.tbody_element),
|
||||
),
|
||||
);
|
||||
|
||||
hunt_optimizer_store.wanted_items.observe_list(this.update_table);
|
||||
hunt_optimizer_store.wanted_items.observe_list(this.update_table, { call_now: true });
|
||||
}
|
||||
|
||||
private update_table = (): void => {
|
||||
this.tbody_element.append(
|
||||
...hunt_optimizer_store.wanted_items.val.map(wanted_item => {
|
||||
const remove_button = this.table_disposer.add(
|
||||
new Button("", { icon_left: Icon.Remove }),
|
||||
);
|
||||
private update_table = (change: ListPropertyChangeEvent<WantedItemModel>): void => {
|
||||
switch (change.type) {
|
||||
case ListChangeType.Insertion:
|
||||
{
|
||||
const rows = change.inserted.map(this.create_row);
|
||||
|
||||
return el.tr(
|
||||
{},
|
||||
el.td({ text: wanted_item.amount.toString() }),
|
||||
el.td({ text: wanted_item.item_type.name }),
|
||||
el.td({}, remove_button.element),
|
||||
);
|
||||
}),
|
||||
if (change.from >= this.tbody_element.childElementCount) {
|
||||
this.tbody_element.append(...rows);
|
||||
} else {
|
||||
for (let i = change.from; i < change.to; i++) {
|
||||
this.tbody_element.children[i].insertAdjacentElement(
|
||||
"afterend",
|
||||
rows[i - change.from],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ListChangeType.Removal:
|
||||
for (let i = change.from; i < change.to; i++) {
|
||||
this.tbody_element.children[change.from].remove();
|
||||
}
|
||||
break;
|
||||
|
||||
case ListChangeType.Replacement:
|
||||
{
|
||||
const rows = change.inserted.map(this.create_row);
|
||||
|
||||
for (let i = change.from; i < change.removed_to; i++) {
|
||||
this.tbody_element.children[change.from].remove();
|
||||
}
|
||||
|
||||
if (change.from >= this.tbody_element.childElementCount) {
|
||||
this.tbody_element.append(...rows);
|
||||
} else {
|
||||
for (let i = change.from; i < change.inserted_to; i++) {
|
||||
this.tbody_element.children[i].insertAdjacentElement(
|
||||
"afterend",
|
||||
rows[i - change.from],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ListChangeType.Update:
|
||||
// TODO: update row
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private create_row = (wanted_item: WantedItemModel): HTMLTableRowElement => {
|
||||
const amount_input = this.table_disposer.add(
|
||||
new NumberInput(wanted_item.amount.val, { min: 1, step: 1 }),
|
||||
);
|
||||
|
||||
this.table_disposer.add_all(
|
||||
amount_input.value.bind_to(wanted_item.amount),
|
||||
amount_input.value.observe(({ value }) => wanted_item.set_amount(value)),
|
||||
);
|
||||
|
||||
const remove_button = this.table_disposer.add(new Button("", { icon_left: Icon.Remove }));
|
||||
|
||||
return el.tr(
|
||||
{},
|
||||
el.td({}, amount_input.element),
|
||||
el.td({ text: wanted_item.item_type.name }),
|
||||
el.td({}, remove_button.element),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -2,9 +2,26 @@ import { ItemType } from "../../core/model/items";
|
||||
import { DifficultyModel, SectionIdModel } from "../../core/model";
|
||||
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
|
||||
import { Duration } from "luxon";
|
||||
import { Property } from "../../core/observable/property/Property";
|
||||
import { WritableProperty } from "../../core/observable/property/WritableProperty";
|
||||
import { property } from "../../core/observable";
|
||||
|
||||
export class WantedItemModel {
|
||||
constructor(readonly item_type: ItemType, readonly amount: number) {}
|
||||
readonly item_type: ItemType;
|
||||
readonly amount: Property<number>;
|
||||
|
||||
private readonly _amount: WritableProperty<number>;
|
||||
|
||||
constructor(item_type: ItemType, amount: number) {
|
||||
this.item_type = item_type;
|
||||
this._amount = property(amount);
|
||||
this.amount = this._amount;
|
||||
}
|
||||
|
||||
set_amount(amount: number): this {
|
||||
this._amount.val = amount;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class OptimalResultModel {
|
||||
|
@ -13,7 +13,7 @@ class HuntOptimizerPersister extends Persister {
|
||||
wanted_items.map(
|
||||
({ item_type, amount }): PersistedWantedItem => ({
|
||||
itemTypeId: item_type.id,
|
||||
amount,
|
||||
amount: amount.val,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
@ -35,7 +35,9 @@ class HuntOptimizerStore {
|
||||
readonly wanted_items: ListProperty<WantedItemModel>;
|
||||
readonly result: Property<OptimalResultModel | undefined>;
|
||||
|
||||
private readonly _wanted_items: WritableListProperty<WantedItemModel> = list_property();
|
||||
private readonly _wanted_items: WritableListProperty<WantedItemModel> = list_property(
|
||||
wanted_item => [wanted_item.amount],
|
||||
);
|
||||
private readonly _result: WritableProperty<OptimalResultModel | undefined> = property(
|
||||
undefined,
|
||||
);
|
||||
@ -67,7 +69,7 @@ class HuntOptimizerStore {
|
||||
// Initialize this set before awaiting data, so user changes don't affect this optimization
|
||||
// run from this point on.
|
||||
const wanted_items = new Set(
|
||||
this.wanted_items.val.filter(w => w.amount > 0).map(w => w.item_type),
|
||||
this.wanted_items.val.filter(w => w.amount.val > 0).map(w => w.item_type),
|
||||
);
|
||||
|
||||
const methods = await hunt_method_stores.current.val.methods.promise;
|
||||
@ -77,7 +79,7 @@ class HuntOptimizerStore {
|
||||
const constraints: { [item_name: string]: { min: number } } = {};
|
||||
|
||||
for (const wanted of this.wanted_items.val) {
|
||||
constraints[wanted.item_type.name] = { min: wanted.amount };
|
||||
constraints[wanted.item_type.name] = { min: wanted.amount.val };
|
||||
}
|
||||
|
||||
// Add a variable to the LP model per method per difficulty per section ID.
|
||||
|
Loading…
Reference in New Issue
Block a user