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

View File

@ -1,5 +1,9 @@
package com.smallhacker.disbrowser package com.smallhacker.disbrowser
import kotlin.reflect.KProperty
typealias InnerHtml = HtmlArea.() -> Unit
interface HtmlNode { interface HtmlNode {
fun print(): String { fun print(): String {
val out = StringBuilder() 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>() private val children = ArrayList<HtmlNode>()
init {
inner(HtmlArea(this))
}
override fun printTo(out: StringBuilder): StringBuilder { override fun printTo(out: StringBuilder): StringBuilder {
super.printTo(out) super.printTo(out)
children.forEach { it.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) } 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) 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 printTo(out: StringBuilder) = out.append(text)
override fun attr(key: String, value: String?) = this override fun attr(key: String, value: String?) = this
override fun append(node: HtmlNode): HtmlNode = 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>() private val children = ArrayList<HtmlNode>()
init { 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 printTo(out: StringBuilder) = out.apply { children.forEach { it.printTo(out) } }
override fun append(node: HtmlNode) = apply { children.add(node) } override fun append(node: HtmlNode) = apply { children.add(node) }
}
fun HtmlArea.fragment(inner: InnerHtml = {}) = com.smallhacker.disbrowser.fragment(inner).appendTo(parent) override fun toString(): String = print()
fun html(inner: InnerHtml = {}) = parent("html", inner) }
fun HtmlArea.html(inner: InnerHtml = {}) = com.smallhacker.disbrowser.html(inner).appendTo(parent) fun HtmlArea.text(text: String) = HtmlTextNode(text).appendTo(parent)
fun head(inner: InnerHtml = {}) = parent("head", inner) val HtmlArea.fragment get() = htmlFragment().appendTo(parent)
fun HtmlArea.head(inner: InnerHtml = {}) = com.smallhacker.disbrowser.head(inner).appendTo(parent) val HtmlArea.html by ParentBuilder
fun title(inner: InnerHtml = {}) = parent("title", inner) val HtmlArea.head by ParentBuilder
fun HtmlArea.title(inner: InnerHtml = {}) = com.smallhacker.disbrowser.title(inner).appendTo(parent) val HtmlArea.title by ParentBuilder
fun link(inner: InnerHtml = {}) = leaf("link") val HtmlArea.link by LeafBuilder
fun HtmlArea.link(inner: InnerHtml = {}) = com.smallhacker.disbrowser.link(inner).appendTo(parent) val HtmlArea.meta by LeafBuilder
fun meta(inner: InnerHtml = {}) = leaf("meta") val HtmlArea.body by ParentBuilder
fun HtmlArea.meta(inner: InnerHtml = {}) = com.smallhacker.disbrowser.meta(inner).appendTo(parent) val HtmlArea.main by ParentBuilder
fun body(inner: InnerHtml = {}) = parent("body", inner) val HtmlArea.aside by ParentBuilder
fun HtmlArea.body(inner: InnerHtml = {}) = com.smallhacker.disbrowser.body(inner).appendTo(parent) val HtmlArea.div by ParentBuilder
fun div(inner: InnerHtml = {}) = parent("div", inner) val HtmlArea.span by ParentBuilder
fun HtmlArea.div(inner: InnerHtml = {}) = com.smallhacker.disbrowser.div(inner).appendTo(parent) val HtmlArea.table by ParentBuilder
fun span(inner: InnerHtml = {}) = parent("span", inner) val HtmlArea.tr by ParentBuilder
fun HtmlArea.span(inner: InnerHtml = {}) = com.smallhacker.disbrowser.span(inner).appendTo(parent) val HtmlArea.td by ParentBuilder
fun table(inner: InnerHtml = {}) = parent("table", inner) val HtmlArea.a by ParentBuilder
fun HtmlArea.table(inner: InnerHtml = {}) = com.smallhacker.disbrowser.table(inner).appendTo(parent) val HtmlArea.script by ParentBuilder
fun tr(inner: InnerHtml = {}) = parent("tr", inner) val HtmlArea.input by LeafBuilder
fun HtmlArea.tr(inner: InnerHtml = {}) = com.smallhacker.disbrowser.tr(inner).appendTo(parent) val HtmlArea.button by ParentBuilder
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)
fun HtmlNode.appendTo(node: HtmlNode) = apply { node.append(this) } fun HtmlNode.appendTo(node: HtmlNode) = apply { node.append(this) }
fun HtmlNode.addClass(c: String?) = attrAdd("class", c) 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(" ") .split(" ")
.asSequence() .asSequence()
.filterNot { it.isEmpty() } .filterNot { it.isEmpty() }
.toMutableSet() .toMutableSet()
fun HtmlNode.attrAdd(name: String, value: String?) = apply { private fun HtmlNode.attrAdd(name: String, value: String?) = apply {
if (value != null) { if (value != null) {
val set = attrSet(name) val set = attrSet(name)
set.add(value) set.add(value)

View File

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

View File

@ -105,7 +105,7 @@ tr.line-active {
width: 500px; width: 500px;
} }
.opcode-info{ .opcode-info {
text-decoration: green underline dotted; text-decoration: green underline dotted;
} }
@ -125,56 +125,97 @@ tr.line-active {
.field-editable-popup-icon:hover { .field-editable-popup-icon:hover {
font-weight: bold; font-weight: bold;
} }
.field-editable-popup-icon::before { .field-editable-popup-icon::before {
content: "[e]" content: "[e]"
} }
[row-certainty="0"],[row-certainty="1"],[row-certainty="2"],[row-certainty="3"],[row-certainty="4"], [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"] { [row-certainty="5"], [row-certainty="6"], [row-certainty="7"], [row-certainty="8"], [row-certainty="9"] {
color: rgb(200,200,200); color: rgb(200, 200, 200);
} }
[row-certainty="10"],[row-certainty="11"],[row-certainty="12"],[row-certainty="13"],[row-certainty="14"], [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"] { [row-certainty="15"], [row-certainty="16"], [row-certainty="17"], [row-certainty="18"], [row-certainty="19"] {
color: rgb(180,180,180); color: rgb(180, 180, 180);
} }
[row-certainty="20"],[row-certainty="21"],[row-certainty="22"],[row-certainty="23"],[row-certainty="24"], [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"] { [row-certainty="25"], [row-certainty="26"], [row-certainty="27"], [row-certainty="28"], [row-certainty="29"] {
color: rgb(160,160,160); color: rgb(160, 160, 160);
} }
[row-certainty="30"],[row-certainty="31"],[row-certainty="32"],[row-certainty="33"],[row-certainty="34"], [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"] { [row-certainty="35"], [row-certainty="36"], [row-certainty="37"], [row-certainty="38"], [row-certainty="39"] {
color: rgb(140,140,140); color: rgb(140, 140, 140);
} }
[row-certainty="40"],[row-certainty="41"],[row-certainty="42"],[row-certainty="43"],[row-certainty="44"], [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"] { [row-certainty="45"], [row-certainty="46"], [row-certainty="47"], [row-certainty="48"], [row-certainty="49"] {
color: rgb(120,120,120); color: rgb(120, 120, 120);
} }
[row-certainty="50"],[row-certainty="51"],[row-certainty="52"],[row-certainty="53"],[row-certainty="54"], [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"] { [row-certainty="55"], [row-certainty="56"], [row-certainty="57"], [row-certainty="58"], [row-certainty="59"] {
color: rgb(100,100,100); color: rgb(100, 100, 100);
} }
[row-certainty="60"],[row-certainty="61"],[row-certainty="62"],[row-certainty="63"],[row-certainty="64"], [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"] { [row-certainty="65"], [row-certainty="66"], [row-certainty="67"], [row-certainty="68"], [row-certainty="69"] {
color: rgb(80,80,80); color: rgb(80, 80, 80);
} }
[row-certainty="70"],[row-certainty="71"],[row-certainty="72"],[row-certainty="73"],[row-certainty="74"], [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"] { [row-certainty="75"], [row-certainty="76"], [row-certainty="77"], [row-certainty="78"], [row-certainty="79"] {
color: rgb(60,60,60); color: rgb(60, 60, 60);
} }
[row-certainty="80"],[row-certainty="81"],[row-certainty="82"],[row-certainty="83"],[row-certainty="84"], [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"] { [row-certainty="85"], [row-certainty="86"], [row-certainty="87"], [row-certainty="88"], [row-certainty="89"] {
color: rgb(40,40,40); color: rgb(40, 40, 40);
} }
[row-certainty="90"],[row-certainty="91"],[row-certainty="92"],[row-certainty="93"],[row-certainty="94"], [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"] { [row-certainty="95"], [row-certainty="96"], [row-certainty="97"], [row-certainty="98"], [row-certainty="99"] {
color: rgb(20,20,20); 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; 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;
})
} }