// Copyright Epic Games, Inc. All Rights Reserved. "use strict"; import { Component } from "./component.js" import { Friendly } from "../util/friendly.js" //////////////////////////////////////////////////////////////////////////////// class Widget extends Component { } //////////////////////////////////////////////////////////////////////////////// class TableCell extends Widget { constructor(element, row) { super(element); this._row = row; } get_table() { return this.get_row().get_table(); } get_row() { return this._row; } } //////////////////////////////////////////////////////////////////////////////// class TableRow extends Widget { constructor(element, table, index, cells) { super(element); this._table = table; this._index = index; this._cells = cells; } *[Symbol.iterator]() { for (var cell of this._cells) yield cell; } get_table() { return this._table; } get_index() { return this._index; } get_cell(index) { return this._cells.at(index); } } //////////////////////////////////////////////////////////////////////////////// export class Table extends Widget { static Flag_EvenSpacing = 1 << 0; static Flag_PackRight = 1 << 1; static Flag_BiasLeft = 1 << 2; static Flag_FitLeft = 1 << 3; static Flag_Sortable = 1 << 4; static Flag_AlignNumeric = 1 << 5; constructor(parent, column_names, flags=Table.Flag_EvenSpacing, index_base=0) { var root = parent.tag().classify("zen_table"); super(root); const column_width = 0 | (100 / column_names.length); var column_style; if (flags & Table.Flag_FitLeft) column_style = "max-content"; else if (column_names.length == 1) column_style = "1fr"; else if (flags & Table.Flag_BiasLeft) column_style = `minmax(${column_width * 2}%, 1fr)`; else column_style = `minmax(${column_width}%, 1fr)`; for (var i = 1; i < column_names.length; ++i) { const style = (flags & Table.Flag_PackRight) ? " auto" : " 1fr"; column_style += style; } if (index_base >= 0) { column_names = ["#", ...column_names]; column_style = "max-content " + column_style; } root.style("gridTemplateColumns", column_style); this._header_row = this._add_row(column_names, false); this._index = index_base; this._num_columns = column_names.length; this._rows = []; this._flags = flags; this._sort_column = -1; this._sort_ascending = true; if (flags & Table.Flag_Sortable) { this._init_sortable(); } } _init_sortable() { const header_elem = this._element.firstElementChild; if (!header_elem) { return; } const cells = header_elem.children; for (let i = 0; i < cells.length; i++) { const cell = cells[i]; cell.style.cursor = "pointer"; cell.style.userSelect = "none"; cell.addEventListener("click", () => this._sort_by(i)); } } _sort_by(column_index) { if (this._sort_column === column_index) { this._sort_ascending = !this._sort_ascending; } else { this._sort_column = column_index; this._sort_ascending = true; } // Update header indicators const header_elem = this._element.firstElementChild; for (const cell of header_elem.children) { const text = cell.textContent.replace(/ [▲▼]$/, ""); cell.textContent = text; } const active_cell = header_elem.children[column_index]; active_cell.textContent += this._sort_ascending ? " ▲" : " ▼"; // Sort rows by comparing cell text content const dir = this._sort_ascending ? 1 : -1; const unit_multipliers = { "B": 1, "KiB": 1024, "MiB": 1048576, "GiB": 1073741824, "TiB": 1099511627776, "PiB": 1125899906842624, "EiB": 1152921504606846976 }; const parse_sortable = (text) => { // Try byte units first (e.g. "1,234 KiB", "1.5 GiB") const byte_match = text.match(/^([\d,.]+)\s*(B|[KMGTPE]iB)/); if (byte_match) { const num = parseFloat(byte_match[1].replace(/,/g, "")); const mult = unit_multipliers[byte_match[2]] || 1; return num * mult; } // Try percentage (e.g. "95.5%") const pct_match = text.match(/^([\d,.]+)%/); if (pct_match) { return parseFloat(pct_match[1].replace(/,/g, "")); } // Try plain number (possibly with commas/separators) const num = parseFloat(text.replace(/,/g, "")); if (!isNaN(num)) { return num; } return null; }; this._rows.sort((a, b) => { const aElem = a.inner().children[column_index]; const bElem = b.inner().children[column_index]; const aText = aElem ? aElem.textContent : ""; const bText = bElem ? bElem.textContent : ""; const aNum = parse_sortable(aText); const bNum = parse_sortable(bText); if (aNum !== null && bNum !== null) { return (aNum - bNum) * dir; } return aText.localeCompare(bText) * dir; }); // Re-order DOM elements for (const row of this._rows) { this._element.appendChild(row.inner()); } } *[Symbol.iterator]() { for (var row of this._rows) yield row; } get_row(index) { return this._rows.at(index); } _add_row(cells, indexed=true) { var index = -1; if (indexed && this._index >= 0) { index = this._index++; cells = [index, ...cells]; } cells = cells.slice(0, this._num_columns); while (cells.length < this._num_columns) cells.push(""); var ret = []; var row = this.tag(); row = new TableRow(row, this, index, ret); for (const cell of cells) { var leaf = row.tag().text(cell); ret.push(new TableCell(leaf, row)); } if ((this._flags & Table.Flag_AlignNumeric) && indexed) { for (const c of ret) { const t = c.inner().textContent; if (t && /^\d/.test(t)) { c.style("textAlign", "right"); } } } if (this._index >= 0) ret.shift(); return row; } add_row(...args) { var row = this._add_row(args); this._rows.push(row); if ((this._flags & Table.Flag_AlignNumeric) && this._rows.length === 1) { this._align_header(); } return row; } _align_header() { const first_row = this._rows[0]; if (!first_row) { return; } const header_elem = this._element.firstElementChild; const header_cells = header_elem.children; const data_cells = first_row.inner().children; for (let i = 0; i < data_cells.length && i < header_cells.length; i++) { if (data_cells[i].style.textAlign === "right") { header_cells[i].style.textAlign = "right"; } } } clear(index=0) { const elem = this._element; elem.replaceChildren(elem.firstElementChild); this._index = (this._index >= 0) ? index : -1; this._rows = []; } } //////////////////////////////////////////////////////////////////////////////// export class PropTable extends Table { constructor(parent) { super(parent, ["prop", "value"], Table.Flag_FitLeft, -1); this.classify("zen_proptable"); } add_property(key, value) { return this.add_row(key, value); } add_object(object, friendly=false, prec=2) { const impl = (node, prefix="") => { for (const key in node) { var value = node[key]; if (value instanceof Object && (value.constructor.name == "Object" || value.constructor.name == "Array")) { impl(value, prefix + key + "."); continue; } if (friendly && ((typeof value == "number") || (typeof value == "bigint"))) { if (key.indexOf("memory") >= 0) value = Friendly.kib(value); else if (key.indexOf("disk") >= 0) value = Friendly.kib(value); else if (value > 100000) value = Friendly.k(value); else if (value % 1) value = Friendly.sep(value, 3); else value = Friendly.sep(value, 0); } this.add_property(prefix + key, value); } }; return impl(object); } filter(...needles) { for (var row of this) row.retag("div"); if (needles.length == 0) return; for (var row of this) { var hide = false; var cell = row.get_cell(0); for (var needle of needles) hide = hide || (cell.inner().innerHTML.indexOf(needle) < 0); if (hide) row.retag("hidden"); } } } //////////////////////////////////////////////////////////////////////////////// export class Toolbar extends Widget { static Side = class extends Widget { add(name, tag="div") { return this.tag(tag).text(name); } sep() { return this.tag().text("|").classify("zen_toolbar_sep"); } } constructor(parent, inline=false) { var root = parent.tag().classify("zen_toolbar"); super(root); if (inline) root.classify("zen_toolbar_inline"); this._left = new Toolbar.Side(root.tag()); this._right = new Toolbar.Side(root.tag()); } left() { return this._left; } right() { return this._right; } } //////////////////////////////////////////////////////////////////////////////// export class ProgressBar extends Widget { constructor(parent) { const root = parent.tag().classify("zen_progressbar"); super(root); this._label = root.tag(); root.tag(); // bg this._bar = root.tag(); } set_progress(what, count=0, end=1) { const percent = (((count * 100) / end) | 0).toString() + "%"; this._bar.style("width", percent); this._label.text(`${what}... ${count}/${end} (${percent})`); } } //////////////////////////////////////////////////////////////////////////////// export class WidgetHost { constructor(parent, depth=1) { this._parent = parent; this._depth = depth; } add_section(name) { var node = this._parent.tag(); if (this._depth == 1) node.classify("zen_sector"); node.tag("h" + this._depth).text(name); return new WidgetHost(node, this._depth + 1); } add_widget(type, ...args) { if (!(type.prototype instanceof Widget)) throw Error("Incorrect widget type"); return new type(this._parent, ...args); } tag(...args) { return this._parent.tag(...args); } }