├── .github └── workflows │ └── deploy-to-gh-pages.yml ├── .gitignore ├── 3rdparty ├── scrollbars.css └── scrollbars.js ├── DEVNOTES.md ├── ImHUI.css ├── LICENSE.md ├── README.md ├── TODO.md ├── index.css ├── index.html ├── package-lock.json ├── package.json ├── src ├── ImHUI.ts ├── background.ts ├── button.ts ├── canvas2D.ts ├── child.ts ├── colorEdit.ts ├── colors.ts ├── context.ts ├── core.ts ├── elements.ts ├── globalState.ts ├── inputText.ts ├── main.ts ├── menu.ts ├── plotLines.ts ├── sliderFloat.ts ├── text.ts ├── types │ └── ResizeObserver.d.ts ├── utils.ts ├── valueDrag.ts └── window.ts └── tsconfig.json /.github/workflows/deploy-to-gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🍔🍟🥤 12 | uses: actions/checkout@v2.3.1 13 | with: 14 | persist-credentials: false 15 | 16 | - name: Use Node.js 😂 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: '14.x' 20 | 21 | - name: Install and Build 🏭 22 | run: | 23 | npm i 24 | npm run build-ci 25 | 26 | - name: Deploy 📦 27 | if: ${{ github.event_name == 'push' }} 28 | uses: JamesIves/github-pages-deploy-action@3.6.2 29 | with: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | BRANCH: gh-pages 32 | FOLDER: build 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | -------------------------------------------------------------------------------- /3rdparty/scrollbars.css: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | :root { 3 | --scrollbar-size: .375rem; 4 | --scrollbar-minlength: 1.5rem; /* Minimum length of scrollbar thumb (width of horizontal, height of vertical) */ 5 | --scrollbar-ff-width: thin; /* FF-only accepts auto, thin, none */ 6 | --scrollbar-track-color: transparent; 7 | --scrollbar-color: rgba(0,0,0,.2); 8 | --scrollbar-color-hover: rgba(0,0,0,.3); 9 | --scrollbar-color-active: rgb(0,0,0); 10 | } 11 | 12 | /* Use .layout-scrollbar-obtrusive to only use overflow if scrollbars don’t overlay */ 13 | .scrollbar-test, 14 | .layout-cell { 15 | overscroll-behavior: contain; 16 | overflow-y: auto; 17 | -webkit-overflow-scrolling: touch; 18 | -ms-overflow-style: -ms-autohiding-scrollbar; 19 | scrollbar-width: var(--scrollbar-ff-width); 20 | } 21 | 22 | /* This class controls what elements have the new fancy scrollbar CSS */ 23 | .layout-scrollbar { 24 | scrollbar-color: var(--scrollbar-color) var(--scrollbar-track-color); 25 | } 26 | /* Only apply height/width to ::-webkit-scrollbar if is obtrusive */ 27 | .layout-scrollbar-obtrusive .layout-scrollbar::-webkit-scrollbar { 28 | height: var(--scrollbar-size); 29 | width: var(--scrollbar-size); 30 | } 31 | .layout-scrollbar::-webkit-scrollbar-track { 32 | background-color: var(--scrollbar-track-color); 33 | } 34 | .layout-scrollbar::-webkit-scrollbar-thumb { 35 | background-color: var(--scrollbar-color); 36 | border-radius: 3px; 37 | } 38 | .layout-scrollbar::-webkit-scrollbar-thumb:hover { 39 | background-color: var(--scrollbar-color-hover); 40 | } 41 | .layout-scrollbar::-webkit-scrollbar-thumb:active { 42 | background-color: var(--scrollbar-color-active); 43 | } 44 | .scrollbar-test::-webkit-scrollbar-thumb:vertical, 45 | .layout-scrollbar::-webkit-scrollbar-thumb:vertical { 46 | min-height: var(--scrollbar-minlength); 47 | } 48 | .scrollbar-test::-webkit-scrollbar-thumb:horizontal, 49 | .layout-scrollbar::-webkit-scrollbar-thumb:horizontal { 50 | min-width: var(--scrollbar-minlength); 51 | } 52 | 53 | 54 | @media (prefers-color-scheme: dark) { 55 | :root { 56 | --scrollbar-color:#555; 57 | --scrollbar-color-hover: #555; 58 | --scrollbar-color-active: #555; 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /3rdparty/scrollbars.js: -------------------------------------------------------------------------------- 1 | //import './scrollbars.css'; 2 | 3 | /* 4 | * Scrollbar Width Test 5 | * Adds `layout-scrollbar-obtrusive` class to body if scrollbars use up screen real estate 6 | */ 7 | const parent = document.createElement("div"); 8 | parent.setAttribute("style", "width:30px;height:30px;"); 9 | parent.classList.add('scrollbar-test'); 10 | 11 | const child = document.createElement("div"); 12 | child.setAttribute("style", "width:100%;height:40px"); 13 | parent.appendChild(child); 14 | document.body.appendChild(parent); 15 | 16 | // Measure the child element, if it is not 17 | // 30px wide the scrollbars are obtrusive. 18 | const scrollbarWidth = 30 - parent.firstChild.clientWidth; 19 | if(scrollbarWidth) { 20 | document.body.classList.add("layout-scrollbar-obtrusive"); 21 | } 22 | 23 | document.body.removeChild(parent); -------------------------------------------------------------------------------- /DEVNOTES.md: -------------------------------------------------------------------------------- 1 | # Development Notes 2 | 3 | ## How it works ATM 4 | 5 | There is a hierarchy of `Node` (an internal type). 6 | While rendering, we get existing nodes in the order they 7 | were rendering last time. We compare the node we got to 8 | the type of node we want. In other words, if we have top level 9 | code like 10 | 11 | ```js 12 | function renderUI() { 13 | text('Editor'); 14 | speed = sliderFloat('speed', speed); 15 | title = inputText('title', title); 16 | } 17 | ``` 18 | 19 | Then 3 `Node`s are generated, a `TextNode`, a `SliderFloatNode`, 20 | and a `InputTextNode` in that order 21 | 22 | The next time through when we see `text('Editor')` we'll get 23 | the first `Node` and check if it's a `TextNode`. If it is we'll 24 | use it. It should function for any text and any things that need 25 | to updated, should be updated in its `update` method. 26 | 27 | If the node is node `TextNode` then it's discarded. 28 | 29 | This probably means if you change a node near the top of a list 30 | in the hierarchy then everything after it is going to be discarded 31 | and regenerated. 32 | 33 | ATM that's fine just to get things working I think. In the future 34 | we could consider a cache of unused nodes by type and/or some other 35 | ways of optimizing churn. 36 | 37 | ## We need a way to know if a node can be reused 38 | 39 | Guidelines 40 | 41 | * If you allow the user to set `className` then you're update function 42 | must set the className with `this.setClassName(className)` 43 | 44 | * Every part of an element you let a user change you have to check 45 | in `update` if it it's changed 46 | 47 | * If you allow the user to pass the element type then we need a way 48 | to pass that type all the way into `getExistingNodeOrRemove` 49 | so that it can check if an existing node of type X is covering 50 | an element if the required element type 51 | 52 | One idea. We could generate types by element type? Unfortunately 53 | that would not be typescript friendly? 54 | 55 | ```js 56 | class TypeTextNode extends Node { 57 | #text: string; 58 | 59 | constructor(type: string) { 60 | // FIX! You can't pass the type OR we need to fix the code 61 | // that gets an old node because it checks by JS class instanceof 62 | // not by element type 63 | super(type); 64 | } 65 | update(str: string) { 66 | if (this.#text !== str) { 67 | this.#text = str; 68 | this.elem.textContent = str; 69 | } 70 | } 71 | } 72 | 73 | const elementTypeToConstructor = new Map(); 74 | 75 | /** 76 | * Makes one class inherit from another. 77 | * @param {!Object} subClass Class that wants to inherit. 78 | * @param {!Object} superClass Class to inherit from. 79 | */ 80 | function inherit(subClass, superClass) { 81 | /** 82 | * TmpClass. 83 | * @ignore 84 | * @constructor 85 | */ 86 | const TmpClass = function() { }; 87 | TmpClass.prototype = superClass.prototype; 88 | subClass.prototype = new TmpClass(); 89 | }; 90 | 91 | export function typeText(type: string, str: string) { 92 | let ctor = elementTypeToConstructor(type); 93 | if (!ctor) { 94 | ctor = function() { 95 | TextTypeNode.call(type); 96 | } 97 | inherit(ctor, TextTypeNode); 98 | elementTypeToConstructor.set(type, ctor) 99 | } 100 | 101 | // this is the iffy part VVVVV 102 | const node = context.getExistingNodeOrRemove(ctor, type, str); 103 | node.update(str); 104 | } 105 | ``` 106 | 107 | ## Decide how to handle stateful stuff 108 | 109 | Example: A window has the state of position and size. 110 | We really need to expose that to the use. Suggestion 111 | 1 is by ID. (I think ImGUI does it that way). Suggestion 112 | 2 is have the user provide it. 113 | 114 | Advantage to ID is it's opaque. What's saved is not up to the 115 | user. Advantage to exposing it is user can set and save it. 116 | 117 | For now going to try exposing -------------------------------------------------------------------------------- /ImHUI.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-bg-color: #eee; 3 | --main-fg-color: #000; 4 | --accent-bg-color: #CCC; 5 | --outline-color: #888; 6 | --button-bg-color: #48F; 7 | --window-bg-color: #fff; 8 | --hover-bg-color: #F00; 9 | --focus-bg-color: #FF9; 10 | 11 | --title-bg-color: #CCC; 12 | --link-color: #36C; 13 | --code-area-bg-color: #CCC; 14 | --un-run-bg-color: #8AF; 15 | --aborted-bg-color: #DDD; 16 | --error-fg-color: darkred; 17 | --warn-fg-color: brown; 18 | --editor-bg-color: #fff; 19 | } 20 | 21 | @media (prefers-color-scheme: dark) { 22 | :root { 23 | --main-bg-color: #444; 24 | --main-fg-color: #fff; 25 | --accent-bg-color: #222; 26 | --outline-color: #888; 27 | --button-bg-color: #05f; 28 | --window-bg-color: #444; 29 | --hover-bg-color: #F00; 30 | --focus-bg-color: #000; 31 | 32 | --title-bg-color: #222; 33 | --link-color: #6CF; 34 | --code-area-bg-color: #555; 35 | --un-run-bg-color: blue; 36 | --aborted-bg-color: #000; 37 | --error-fg-color: red; 38 | --warn-fg-color: yellow; 39 | --editor-bg-color: #263238; 40 | } 41 | } 42 | 43 | #root, html, body { 44 | width: 100%; 45 | height: 100%; 46 | margin: 0; 47 | } 48 | #root { 49 | overflow: auto; 50 | padding: 5px; 51 | } 52 | html { 53 | box-sizing: border-box; 54 | background-color: var(--main-bg-color); 55 | color: var(--main-fg-color); 56 | } 57 | html, input, button { 58 | font-family: monospace; 59 | } 60 | button { 61 | background-color: var(--button-bg-color); 62 | color: var(--main-fg-color); 63 | border: 1px solid var(--outline-color); 64 | } 65 | button:hover { 66 | background-color: var(--hover-bg-color); 67 | } 68 | input { 69 | background-color: var(--accent-bg-color); 70 | color: var(--main-fg-color); 71 | border: none; 72 | } 73 | input:focus { 74 | background: var(--focus-bg-color); 75 | } 76 | 77 | *, *:before, *:after { 78 | box-sizing: inherit; 79 | } 80 | canvas { 81 | display: block; 82 | } 83 | 84 | .fill-space { 85 | width: 100%; 86 | height: 100%; 87 | } 88 | 89 | .value-drag { 90 | cursor: pointer; 91 | user-select: none; 92 | margin-right: 3px; 93 | padding-left: 3px; 94 | padding-right: 3px; 95 | background-color: var(--accent-bg-color); 96 | } 97 | .child { 98 | max-height: 10em; 99 | overflow: auto; 100 | border: 1px solid var(--outline-color); 101 | padding: 2px; 102 | } 103 | 104 | .window { 105 | position: absolute; 106 | box-shadow: 5px 5px 5px black; 107 | border: 1px solid black; 108 | resize: both; 109 | padding: 3px; 110 | overflow: auto; 111 | background-color: var(--window-bg-color); 112 | opacity: 0.8; 113 | } 114 | .window>details>summary { 115 | margin: 3px; 116 | user-select: none; 117 | cursor: pointer; 118 | } 119 | /* --------------------------- */ 120 | .form-line { 121 | display: flex; 122 | position: relative; 123 | margin: 1px; 124 | } 125 | .form-line>*:nth-child(1) { 126 | flex: 1 1 auto; 127 | } 128 | .form-line>*:nth-child(2) { 129 | flex: 0 0 9em; 130 | } 131 | /* --------------------------- */ 132 | .plot-lines { 133 | height: 2em; 134 | } 135 | .plot-lines canvas { 136 | background: var(--accent-bg-color); 137 | } 138 | /* --------------------------- */ 139 | .color-edit-4-sub { 140 | display: flex; 141 | } 142 | .color-edit-4-sub>div:nth-child(1), 143 | .color-edit-4-sub>div:nth-child(2), 144 | .color-edit-4-sub>div:nth-child(3), 145 | .color-edit-4-sub>div:nth-child(4) { 146 | flex: 1 1 auto; 147 | text-align: center; 148 | } 149 | 150 | .color-button { 151 | width: 1em; 152 | height: 1em; 153 | border: 1px solid black; 154 | } 155 | /* --------------------------- */ 156 | /* wrapper for slider and value */ 157 | .slider-value { 158 | position: relative; 159 | } 160 | .slider-value>div { 161 | position: absolute; 162 | left: 0; 163 | top: 0; 164 | text-align: center; 165 | width: 100%; 166 | user-select: none; 167 | pointer-events: none; 168 | } 169 | .slider-value>input { 170 | position: absolute; 171 | left: 0; 172 | top: 0; 173 | width: 100%; 174 | -webkit-appearance: none; 175 | margin: 0; 176 | height: 1.1em; 177 | background: var(--accent-bg-color); 178 | outline: none; 179 | } 180 | .slider-value>input::-webkit-slider-thumb { 181 | -webkit-appearance: none; 182 | height: 0.9em; 183 | width: 1em; 184 | background: var(--button-bg-color); 185 | cursor: pointer; 186 | } 187 | .slider-value>input::-webkit-slider-thumb:hover { 188 | background: var(--hover-bg-color); 189 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gregg Tavares 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImHUI (**I**mmediate **M**ode **H**TML **U**ser **I**nterface) 2 | 3 | [Live Demo](https://greggman.github.io/ImHUI) 4 | 5 | **WAT?** I'm a fan (and a sponsor) of [Dear ImGUI](https://github.com/ocornut/imgui). I've written a couple of articles on it including [this one](https://games.greggman.com/game/imgui-future/) and [this one](https://games.greggman.com/game/rethinking-ui-apis/) 6 | 7 | Lately I thought, I wonder what it would be like to try to make an 8 | HTML library that followed a similar style of API. 9 | 10 | NOTE: This is not Dear ImGUI running in JavaScript. For that see 11 | [this repo](https://github.com/flyover/imgui-js). The difference 12 | is most ImGUI libraries render their own graphics. More specifically 13 | they generate arrays of vertex positions, texture coordinates, and 14 | vertex colors for the glyphs and other lines and rectangles for your 15 | UI. You draw each array of vertices using whatever method you feel like. 16 | 17 | This repo is instead actually using HTML elements like `
` 18 | ``, ``, `