Improved HTML DSL, experimental dark mode

This commit is contained in:
Smallhacker 2019-01-15 20:31:05 -05:00
parent 1dd32a2de8
commit 3d7dcc08db
5 changed files with 263 additions and 135 deletions

View File

@ -47,7 +47,9 @@ class Grid {
arrowClasses[x to y] = "arrow arrow-$dir-middle"
}
arrowClasses[x to y2] = "arrow arrow-$dir-end"
arrowCells[x to yEnd] = div().addClass("arrow-head")
arrowCells[x to yEnd] = htmlFragment {
div.addClass("arrow-head")
}
}
private fun nextArrowX(y1: Int, y2: Int): Int {
@ -79,10 +81,14 @@ class Grid {
= ins.print(gameData)
add(y, ins.address,
text(address ?: ""),
text(bytes),
htmlFragment {
text(address ?: "")
},
htmlFragment {
text(bytes)
},
editableField(game, indicativeAddress, "label", label),
fragment {
htmlFragment {
if (secondaryMnemonic == null) {
text(primaryMnemonic)
} else {
@ -113,7 +119,9 @@ class Grid {
}.attr("href", url)
}
},
text(state ?: ""),
htmlFragment {
text(state ?: "")
},
editableField(game, indicativeAddress, "comment", comment)
)
@ -125,12 +133,15 @@ class Grid {
}
private fun editableField(game: Game, address: SnesAddress, type: String, value: String?): HtmlNode {
return input(value = value ?: "")
.addClass("field-$type")
.addClass("field-editable")
.attr("data-field", type)
.attr("data-game", game.id)
.attr("data-address", address.toSimpleString())
return htmlFragment {
input.attr("value", value ?: "")
.attr("type", "text")
.addClass("field-$type")
.addClass("field-editable")
.attr("data-field", type)
.attr("data-game", game.id)
.attr("data-address", address.toSimpleString())
}
}
private fun HtmlArea.editablePopupField(game: Game, address: SnesAddress, type: String, displayValue: String?, editValue: String?) {
@ -148,7 +159,17 @@ class Grid {
private fun addDummy() {
val y = (height++)
add(y, null, null, null, null, text("..."), null, null)
add(y,
null,
null,
null,
null,
htmlFragment {
text("...")
},
null,
null
)
}
private fun add(y: Int, address: SnesAddress?,
@ -176,30 +197,32 @@ class Grid {
.max()
?: -1
return table {
for (y in 0 until height) {
tr {
for (x in 0..3) {
val cssClass = cellClasses[x to y]
td {
content[x to y]?.appendTo(parent)
}.addClass(cssClass)
}
for (x in 0 until arrowWidth) {
val cssClass = arrowClasses[x to y]
td {
arrowCells[x to y]?.appendTo(parent)
}.addClass(cssClass)
}
for (x in 4..contentMaxX) {
val cssClass = cellClasses[x to y]
td {
content[x to y]?.appendTo(parent)
}.addClass(cssClass)
}
}.addClass(rowClasses[y])
.attr("id", rowId[y])
.attr("row-certainty", rowCertainties[y])
return htmlFragment {
table {
for (y in 0 until height) {
tr {
for (x in 0..3) {
val cssClass = cellClasses[x to y]
td {
content[x to y]?.appendTo(parent)
}.addClass(cssClass)
}
for (x in 0 until arrowWidth) {
val cssClass = arrowClasses[x to y]
td {
arrowCells[x to y]?.appendTo(parent)
}.addClass(cssClass)
}
for (x in 4..contentMaxX) {
val cssClass = cellClasses[x to y]
td {
content[x to y]?.appendTo(parent)
}.addClass(cssClass)
}
}.addClass(rowClasses[y])
.attr("id", rowId[y])
.attr("row-certainty", rowCertainties[y])
}
}
}
}

View File

@ -1,5 +1,9 @@
package com.smallhacker.disbrowser
import kotlin.reflect.KProperty
typealias InnerHtml = HtmlArea.() -> Unit
interface HtmlNode {
fun print(): String {
val out = StringBuilder()
@ -40,13 +44,9 @@ open class HtmlElement(protected val tag: String) : HtmlNode {
}
}
private class ParentHtmlElement(tag: String, inner: InnerHtml) : HtmlElement(tag) {
private class ParentHtmlElement(tag: String) : HtmlElement(tag) {
private val children = ArrayList<HtmlNode>()
init {
inner(HtmlArea(this))
}
override fun printTo(out: StringBuilder): StringBuilder {
super.printTo(out)
children.forEach { it.printTo(out) }
@ -57,22 +57,22 @@ private class ParentHtmlElement(tag: String, inner: InnerHtml) : HtmlElement(tag
override fun append(node: HtmlNode) = apply { children.add(node) }
}
private fun parent(tag: String, inner: InnerHtml): HtmlNode = ParentHtmlElement(tag, inner)
private fun leaf(tag: String): HtmlNode = HtmlElement(tag)
typealias InnerHtml = HtmlArea.() -> Unit
class HtmlArea(val parent: HtmlNode)
fun text(text: String): HtmlNode = object : HtmlNode {
private class HtmlTextNode(private val text: String): HtmlNode {
override fun printTo(out: StringBuilder) = out.append(text)
override fun attr(key: String, value: String?) = this
override fun append(node: HtmlNode): HtmlNode = this
}
fun HtmlArea.text(text: String) = com.smallhacker.disbrowser.text(text).appendTo(parent)
private object ParentBuilder {
operator fun getValue(a: HtmlArea, b: KProperty<*>) = ParentHtmlElement(b.name).appendTo(a.parent)
}
private object LeafBuilder {
operator fun getValue(a: HtmlArea, b: KProperty<*>) = HtmlElement(b.name).appendTo(a.parent)
}
fun fragment(inner: InnerHtml = {}) = object : HtmlNode {
fun htmlFragment(inner: InnerHtml = {}) = object : HtmlNode {
private val children = ArrayList<HtmlNode>()
init {
@ -82,48 +82,45 @@ fun fragment(inner: InnerHtml = {}) = object : HtmlNode {
override fun printTo(out: StringBuilder) = out.apply { children.forEach { it.printTo(out) } }
override fun append(node: HtmlNode) = apply { children.add(node) }
}
fun HtmlArea.fragment(inner: InnerHtml = {}) = com.smallhacker.disbrowser.fragment(inner).appendTo(parent)
fun html(inner: InnerHtml = {}) = parent("html", inner)
fun HtmlArea.html(inner: InnerHtml = {}) = com.smallhacker.disbrowser.html(inner).appendTo(parent)
fun head(inner: InnerHtml = {}) = parent("head", inner)
fun HtmlArea.head(inner: InnerHtml = {}) = com.smallhacker.disbrowser.head(inner).appendTo(parent)
fun title(inner: InnerHtml = {}) = parent("title", inner)
fun HtmlArea.title(inner: InnerHtml = {}) = com.smallhacker.disbrowser.title(inner).appendTo(parent)
fun link(inner: InnerHtml = {}) = leaf("link")
fun HtmlArea.link(inner: InnerHtml = {}) = com.smallhacker.disbrowser.link(inner).appendTo(parent)
fun meta(inner: InnerHtml = {}) = leaf("meta")
fun HtmlArea.meta(inner: InnerHtml = {}) = com.smallhacker.disbrowser.meta(inner).appendTo(parent)
fun body(inner: InnerHtml = {}) = parent("body", inner)
fun HtmlArea.body(inner: InnerHtml = {}) = com.smallhacker.disbrowser.body(inner).appendTo(parent)
fun div(inner: InnerHtml = {}) = parent("div", inner)
fun HtmlArea.div(inner: InnerHtml = {}) = com.smallhacker.disbrowser.div(inner).appendTo(parent)
fun span(inner: InnerHtml = {}) = parent("span", inner)
fun HtmlArea.span(inner: InnerHtml = {}) = com.smallhacker.disbrowser.span(inner).appendTo(parent)
fun table(inner: InnerHtml = {}) = parent("table", inner)
fun HtmlArea.table(inner: InnerHtml = {}) = com.smallhacker.disbrowser.table(inner).appendTo(parent)
fun tr(inner: InnerHtml = {}) = parent("tr", inner)
fun HtmlArea.tr(inner: InnerHtml = {}) = com.smallhacker.disbrowser.tr(inner).appendTo(parent)
fun td(inner: InnerHtml = {}) = parent("td", inner)
fun HtmlArea.td(inner: InnerHtml = {}) = com.smallhacker.disbrowser.td(inner).appendTo(parent)
fun a(inner: InnerHtml = {}) = parent("a", inner)
fun HtmlArea.a(inner: InnerHtml = {}) = com.smallhacker.disbrowser.a(inner).appendTo(parent)
fun script(inner: InnerHtml = {}) = parent("script", inner)
fun HtmlArea.script(inner: InnerHtml = {}) = com.smallhacker.disbrowser.script(inner).appendTo(parent)
fun input(type: String = "text", value: String = "") = leaf("input").attr("type", type).attr("value", value)
fun HtmlArea.input(type: String = "text", value: String = "") = com.smallhacker.disbrowser.input(type, value).appendTo(parent)
override fun toString(): String = print()
}
fun HtmlArea.text(text: String) = HtmlTextNode(text).appendTo(parent)
val HtmlArea.fragment get() = htmlFragment().appendTo(parent)
val HtmlArea.html by ParentBuilder
val HtmlArea.head by ParentBuilder
val HtmlArea.title by ParentBuilder
val HtmlArea.link by LeafBuilder
val HtmlArea.meta by LeafBuilder
val HtmlArea.body by ParentBuilder
val HtmlArea.main by ParentBuilder
val HtmlArea.aside by ParentBuilder
val HtmlArea.div by ParentBuilder
val HtmlArea.span by ParentBuilder
val HtmlArea.table by ParentBuilder
val HtmlArea.tr by ParentBuilder
val HtmlArea.td by ParentBuilder
val HtmlArea.a by ParentBuilder
val HtmlArea.script by ParentBuilder
val HtmlArea.input by LeafBuilder
val HtmlArea.button by ParentBuilder
fun HtmlNode.appendTo(node: HtmlNode) = apply { node.append(this) }
fun HtmlNode.addClass(c: String?) = attrAdd("class", c)
val HtmlNode.inner get() = this
operator fun HtmlNode.invoke(inner: InnerHtml): HtmlNode = append(htmlFragment(inner))
fun HtmlNode.attrSet(name: String): MutableSet<String> = (attr(name) ?: "")
fun HtmlNode.attr(key: String, value: String?, inner: InnerHtml) = attr(key, value).inner(inner)
fun HtmlNode.addClass(c: String?, inner: InnerHtml) = addClass(c).inner(inner)
fun HtmlNode.append(node: HtmlNode, inner: InnerHtml) = append(node).inner(inner)
private fun HtmlNode.attrSet(name: String): MutableSet<String> = (attr(name) ?: "")
.split(" ")
.asSequence()
.filterNot { it.isEmpty() }
.toMutableSet()
fun HtmlNode.attrAdd(name: String, value: String?) = apply {
private fun HtmlNode.attrAdd(name: String, value: String?) = apply {
if (value != null) {
val set = attrSet(name)
set.add(value)

View File

@ -25,20 +25,22 @@ class DisassemblyResource {
@Produces(MediaType.TEXT_HTML)
fun getIt(@PathParam("game") gameName: String) = handle {
games.getGame(gameName)?.let { game ->
table {
Service.getVectors(game).forEach {
tr {
td { text(it.name) }
td { text("(" + it.vectorLocation.toFormattedString() + ")") }
td {
a {
text(it.label)
}.attr("href", "/${game.id}/${it.codeLocation.toSimpleString()}/MX")
htmlFragment {
table {
Service.getVectors(game).forEach {
tr {
td { text(it.name) }
td { text("(" + it.vectorLocation.toFormattedString() + ")") }
td {
a {
text(it.label)
}.attr("href", "/${game.id}/${it.codeLocation.toSimpleString()}/MX")
}
td { text("(" + it.codeLocation.toFormattedString() + ")") }
}
td { text("(" + it.codeLocation.toFormattedString() + ")") }
}
}
}.addClass("vector-table")
}.addClass("vector-table")
}
}
}
@ -73,17 +75,29 @@ class DisassemblyResource {
val output = runner()
?: return Response.status(404).build()
val html = html {
head {
title { text("Disassembly Browser") }
link {}.attr("rel", "stylesheet").attr("href", "/resources/style.css")
meta {}.attr("charset", "UTF-8")
}
body {
output.appendTo(parent)
script().attr("src", "/resources/disbrowser.js")
}
}
val html =
htmlFragment {
html {
head {
title { text("Disassembly Browser") }
link.attr("rel", "stylesheet").attr("href", "/resources/style.css")
meta.attr("charset", "UTF-8")
}
body {
main {
output.appendTo(parent)
}
aside.addClass("sidebar") {
button.attr("id", "btn-dark-mode") {
text("Dark Mode")
}
}
script.attr("src", "/resources/disbrowser.js")
}
}
}
return Response.ok(html.toString().toByteArray(StandardCharsets.UTF_8))
.encoding("UTF-8")

View File

@ -105,7 +105,7 @@ tr.line-active {
width: 500px;
}
.opcode-info{
.opcode-info {
text-decoration: green underline dotted;
}
@ -125,56 +125,97 @@ tr.line-active {
.field-editable-popup-icon:hover {
font-weight: bold;
}
.field-editable-popup-icon::before {
content: "[e]"
}
[row-certainty="0"],[row-certainty="1"],[row-certainty="2"],[row-certainty="3"],[row-certainty="4"],
[row-certainty="5"],[row-certainty="6"],[row-certainty="7"],[row-certainty="8"],[row-certainty="9"] {
color: rgb(200,200,200);
[row-certainty="0"], [row-certainty="1"], [row-certainty="2"], [row-certainty="3"], [row-certainty="4"],
[row-certainty="5"], [row-certainty="6"], [row-certainty="7"], [row-certainty="8"], [row-certainty="9"] {
color: rgb(200, 200, 200);
}
[row-certainty="10"],[row-certainty="11"],[row-certainty="12"],[row-certainty="13"],[row-certainty="14"],
[row-certainty="15"],[row-certainty="16"],[row-certainty="17"],[row-certainty="18"],[row-certainty="19"] {
color: rgb(180,180,180);
[row-certainty="10"], [row-certainty="11"], [row-certainty="12"], [row-certainty="13"], [row-certainty="14"],
[row-certainty="15"], [row-certainty="16"], [row-certainty="17"], [row-certainty="18"], [row-certainty="19"] {
color: rgb(180, 180, 180);
}
[row-certainty="20"],[row-certainty="21"],[row-certainty="22"],[row-certainty="23"],[row-certainty="24"],
[row-certainty="25"],[row-certainty="26"],[row-certainty="27"],[row-certainty="28"],[row-certainty="29"] {
color: rgb(160,160,160);
[row-certainty="20"], [row-certainty="21"], [row-certainty="22"], [row-certainty="23"], [row-certainty="24"],
[row-certainty="25"], [row-certainty="26"], [row-certainty="27"], [row-certainty="28"], [row-certainty="29"] {
color: rgb(160, 160, 160);
}
[row-certainty="30"],[row-certainty="31"],[row-certainty="32"],[row-certainty="33"],[row-certainty="34"],
[row-certainty="35"],[row-certainty="36"],[row-certainty="37"],[row-certainty="38"],[row-certainty="39"] {
color: rgb(140,140,140);
[row-certainty="30"], [row-certainty="31"], [row-certainty="32"], [row-certainty="33"], [row-certainty="34"],
[row-certainty="35"], [row-certainty="36"], [row-certainty="37"], [row-certainty="38"], [row-certainty="39"] {
color: rgb(140, 140, 140);
}
[row-certainty="40"],[row-certainty="41"],[row-certainty="42"],[row-certainty="43"],[row-certainty="44"],
[row-certainty="45"],[row-certainty="46"],[row-certainty="47"],[row-certainty="48"],[row-certainty="49"] {
color: rgb(120,120,120);
[row-certainty="40"], [row-certainty="41"], [row-certainty="42"], [row-certainty="43"], [row-certainty="44"],
[row-certainty="45"], [row-certainty="46"], [row-certainty="47"], [row-certainty="48"], [row-certainty="49"] {
color: rgb(120, 120, 120);
}
[row-certainty="50"],[row-certainty="51"],[row-certainty="52"],[row-certainty="53"],[row-certainty="54"],
[row-certainty="55"],[row-certainty="56"],[row-certainty="57"],[row-certainty="58"],[row-certainty="59"] {
color: rgb(100,100,100);
[row-certainty="50"], [row-certainty="51"], [row-certainty="52"], [row-certainty="53"], [row-certainty="54"],
[row-certainty="55"], [row-certainty="56"], [row-certainty="57"], [row-certainty="58"], [row-certainty="59"] {
color: rgb(100, 100, 100);
}
[row-certainty="60"],[row-certainty="61"],[row-certainty="62"],[row-certainty="63"],[row-certainty="64"],
[row-certainty="65"],[row-certainty="66"],[row-certainty="67"],[row-certainty="68"],[row-certainty="69"] {
color: rgb(80,80,80);
[row-certainty="60"], [row-certainty="61"], [row-certainty="62"], [row-certainty="63"], [row-certainty="64"],
[row-certainty="65"], [row-certainty="66"], [row-certainty="67"], [row-certainty="68"], [row-certainty="69"] {
color: rgb(80, 80, 80);
}
[row-certainty="70"],[row-certainty="71"],[row-certainty="72"],[row-certainty="73"],[row-certainty="74"],
[row-certainty="75"],[row-certainty="76"],[row-certainty="77"],[row-certainty="78"],[row-certainty="79"] {
color: rgb(60,60,60);
[row-certainty="70"], [row-certainty="71"], [row-certainty="72"], [row-certainty="73"], [row-certainty="74"],
[row-certainty="75"], [row-certainty="76"], [row-certainty="77"], [row-certainty="78"], [row-certainty="79"] {
color: rgb(60, 60, 60);
}
[row-certainty="80"],[row-certainty="81"],[row-certainty="82"],[row-certainty="83"],[row-certainty="84"],
[row-certainty="85"],[row-certainty="86"],[row-certainty="87"],[row-certainty="88"],[row-certainty="89"] {
color: rgb(40,40,40);
[row-certainty="80"], [row-certainty="81"], [row-certainty="82"], [row-certainty="83"], [row-certainty="84"],
[row-certainty="85"], [row-certainty="86"], [row-certainty="87"], [row-certainty="88"], [row-certainty="89"] {
color: rgb(40, 40, 40);
}
[row-certainty="90"],[row-certainty="91"],[row-certainty="92"],[row-certainty="93"],[row-certainty="94"],
[row-certainty="95"],[row-certainty="96"],[row-certainty="97"],[row-certainty="98"],[row-certainty="99"] {
color: rgb(20,20,20);
[row-certainty="90"], [row-certainty="91"], [row-certainty="92"], [row-certainty="93"], [row-certainty="94"],
[row-certainty="95"], [row-certainty="96"], [row-certainty="97"], [row-certainty="98"], [row-certainty="99"] {
color: rgb(20, 20, 20);
}
body.dark-mode {
filter: invert(0.9) hue-rotate(180deg);
background-color: black;
}
body {
margin: 0;
position: relative;
overflow: hidden;
}
main {
position: absolute;
left: 0;
right: 50px;
top: 0;
bottom: 0;
padding: 8px;
overflow: auto;
}
.sidebar {
position: absolute;
right: -200px;
top: 0;
bottom: 0;
width: 250px;
z-index: 100;
background-color: lightgray;
border-left: 3px double black;
padding: 10px 10px 10px 50px;
box-sizing: border-box;
transition: right 0.1s;
box-shadow: -1px 0 13px rgba(0,0,0,0.4);
}
.sidebar:hover {
right: 0;
}

View File

@ -107,4 +107,57 @@ for (let i = 0; i < popupEditables.length; i++) {
return false;
});
}
class PersistentProperty<T> {
private _value: T;
private readonly key: string;
private readonly onChange: (value: T) => void;
constructor(key: string, defaultValue: T, onChange: (value: T) => void) {
this.key = key;
this.onChange = onChange;
this.value = this.parse(key, defaultValue);
}
get value(): T {
return this._value;
}
set value(value: T) {
this._value = value;
localStorage.setItem(this.key, JSON.stringify(value));
this.onChange(value);
}
private parse(key: string, defaultValue: T): T {
let value = localStorage.getItem(key);
if (value === null) {
return defaultValue;
}
let parsedValue = JSON.parse(value);
if (parsedValue === null) {
return defaultValue;
}
return parsedValue;
}
}
let darkMode = new PersistentProperty<boolean>(
"ui.presentation.darkMode",
false,
(enable) => {
if (enable) {
document.body.classList.add("dark-mode")
} else {
document.body.classList.remove("dark-mode")
}
}
);
let btnDarkMode = document.getElementById("btn-dark-mode");
if (btnDarkMode) {
btnDarkMode.addEventListener("click", () => {
darkMode.value = !darkMode.value;
})
}