mkui 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mkui/__init__.py +19 -0
- mkui/static/examples/library-js/index.html +96 -0
- mkui/static/examples/standalone-json/app.json +93 -0
- mkui/static/examples/standalone-json/index.html +12 -0
- mkui/static/src/components/app.js +69 -0
- mkui/static/src/components/frame.js +262 -0
- mkui/static/src/components/menubar.js +78 -0
- mkui/static/src/components/statusbar.js +35 -0
- mkui/static/src/components/workspace.js +641 -0
- mkui/static/src/core.js +110 -0
- mkui/static/src/index.js +22 -0
- mkui/static/src/layout/drag.js +139 -0
- mkui/static/src/layout/tree.js +231 -0
- mkui/static/src/mkio-bridge.js +39 -0
- mkui/static/src/widgets/button.js +13 -0
- mkui/static/src/widgets/mkio-table.js +77 -0
- mkui/static/src/widgets/text.js +17 -0
- mkui/static/styles/mkui.css +282 -0
- mkui-0.1.0.dist-info/METADATA +225 -0
- mkui-0.1.0.dist-info/RECORD +23 -0
- mkui-0.1.0.dist-info/WHEEL +5 -0
- mkui-0.1.0.dist-info/licenses/LICENSE +190 -0
- mkui-0.1.0.dist-info/top_level.txt +1 -0
mkui/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""mkui — config-driven web GUI framework with dockable panes."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
static_dir = Path(__file__).parent / "static"
|
|
8
|
+
"""Path to the directory containing mkui's JS, CSS, and example files.
|
|
9
|
+
|
|
10
|
+
Serve this directory from your web server to make mkui available to browsers::
|
|
11
|
+
|
|
12
|
+
# With mkio:
|
|
13
|
+
[static]
|
|
14
|
+
"/mkui" = "<mkui.static_dir>"
|
|
15
|
+
|
|
16
|
+
# With any ASGI/WSGI framework:
|
|
17
|
+
import mkui
|
|
18
|
+
app.mount("/mkui", StaticFiles(directory=mkui.static_dir))
|
|
19
|
+
"""
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>mkui — library mode</title>
|
|
6
|
+
<link rel="stylesheet" href="../../styles/mkui.css" />
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<mkui-app id="app"></mkui-app>
|
|
10
|
+
<script type="module">
|
|
11
|
+
import { registerPaneType } from "../../src/core.js";
|
|
12
|
+
import "../../src/index.js";
|
|
13
|
+
|
|
14
|
+
// A custom pane type — the kind of thing a JS app would register
|
|
15
|
+
// before handing a config to mkui.
|
|
16
|
+
registerPaneType("clock", (spec, app, host) => {
|
|
17
|
+
const el = document.createElement("div");
|
|
18
|
+
el.style.fontFamily = "var(--mkui-font-mono)";
|
|
19
|
+
el.style.fontSize = "40px";
|
|
20
|
+
el.style.textAlign = "center";
|
|
21
|
+
el.style.marginTop = "18%";
|
|
22
|
+
el.style.color = "var(--mkui-accent)";
|
|
23
|
+
host.appendChild(el);
|
|
24
|
+
const tick = () => { el.textContent = new Date().toLocaleTimeString(); };
|
|
25
|
+
tick();
|
|
26
|
+
setInterval(tick, 1000);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const config = {
|
|
30
|
+
app: { title: "Library mode demo", theme: "dark" },
|
|
31
|
+
state: { status: { message: "Built from JS" } },
|
|
32
|
+
menubar: [
|
|
33
|
+
{ label: "App", items: [
|
|
34
|
+
{ label: "Ping status", action: "demo.ping" },
|
|
35
|
+
{ label: "New Notes", action: "demo.newNotes" },
|
|
36
|
+
{ sep: true },
|
|
37
|
+
{ label: "Quit", action: "app.quit" },
|
|
38
|
+
]},
|
|
39
|
+
{ label: "Window", items: [
|
|
40
|
+
{ label: "Tile Horizontal", action: "window.tileH" },
|
|
41
|
+
{ label: "Tile Vertical", action: "window.tileV" },
|
|
42
|
+
{ label: "Tile Grid", action: "window.grid" },
|
|
43
|
+
{ label: "Cascade", action: "window.cascade" },
|
|
44
|
+
]},
|
|
45
|
+
],
|
|
46
|
+
statusbar: {
|
|
47
|
+
left: [{ type: "text", bind: "status.message" }],
|
|
48
|
+
right: [{ type: "text", text: "library mode" }],
|
|
49
|
+
},
|
|
50
|
+
panes: {
|
|
51
|
+
clock: { title: "Clock", type: "clock" },
|
|
52
|
+
notes: { title: "Notes", widgets: [{ type: "text", text: "Hand-built pane. Drag my tab outside to tear me into a new frame, then drop me back on this frame's edge to split it." }] },
|
|
53
|
+
readme: { title: "Readme", widgets: [{ type: "text", text: "Another pane, initially in its own floating frame." }] },
|
|
54
|
+
},
|
|
55
|
+
frames: [
|
|
56
|
+
{
|
|
57
|
+
id: "main",
|
|
58
|
+
x: 0.1, y: 0.1, w: 0.5, h: 0.6,
|
|
59
|
+
layout: { type: "tabs", active: 0, children: ["clock", "notes"] },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "aux",
|
|
63
|
+
x: 0.65, y: 0.2, w: 0.28, h: 0.4,
|
|
64
|
+
layout: { type: "tabs", active: 0, children: ["readme"] },
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const root = document.getElementById("app");
|
|
70
|
+
customElements.whenDefined("mkui-app").then(() => {
|
|
71
|
+
root.setConfig(config);
|
|
72
|
+
const app = root.app;
|
|
73
|
+
app.registerAction("demo.ping", () =>
|
|
74
|
+
app.state.set("status.message", "Pinged at " + new Date().toLocaleTimeString()),
|
|
75
|
+
);
|
|
76
|
+
let notesCounter = 0;
|
|
77
|
+
app.registerAction("demo.newNotes", () => {
|
|
78
|
+
notesCounter += 1;
|
|
79
|
+
const id = `notes-${notesCounter}`;
|
|
80
|
+
app.config.panes[id] = {
|
|
81
|
+
title: `Scratch ${notesCounter}`,
|
|
82
|
+
widgets: [{ type: "text", text: `Scratch pad #${notesCounter}. Created from a menu action.` }],
|
|
83
|
+
};
|
|
84
|
+
// Rebuild the workspace's pane registry and add a new frame for it.
|
|
85
|
+
root.workspace._panes.set(id, app.config.panes[id]);
|
|
86
|
+
root.workspace.addFrame({
|
|
87
|
+
x: 0.2 + (notesCounter % 4) * 0.03,
|
|
88
|
+
y: 0.25 + (notesCounter % 4) * 0.03,
|
|
89
|
+
w: 0.3, h: 0.3,
|
|
90
|
+
layout: { type: "tabs", active: 0, children: [id] },
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
</script>
|
|
95
|
+
</body>
|
|
96
|
+
</html>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app": {
|
|
3
|
+
"title": "mkui demo",
|
|
4
|
+
"theme": "dark"
|
|
5
|
+
},
|
|
6
|
+
"state": {
|
|
7
|
+
"status": { "message": "Ready" }
|
|
8
|
+
},
|
|
9
|
+
"menubar": [
|
|
10
|
+
{
|
|
11
|
+
"label": "File",
|
|
12
|
+
"items": [
|
|
13
|
+
{ "label": "New Frame", "action": "demo.newFrame" },
|
|
14
|
+
{ "sep": true },
|
|
15
|
+
{ "label": "Quit", "action": "app.quit" }
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"label": "Window",
|
|
20
|
+
"items": [
|
|
21
|
+
{ "label": "Tile Horizontal", "action": "window.tileH" },
|
|
22
|
+
{ "label": "Tile Vertical", "action": "window.tileV" },
|
|
23
|
+
{ "label": "Tile Grid", "action": "window.grid" },
|
|
24
|
+
{ "label": "Cascade", "action": "window.cascade" }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"label": "Help",
|
|
29
|
+
"items": [
|
|
30
|
+
{ "label": "About mkui", "action": "demo.about" }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"statusbar": {
|
|
35
|
+
"left": [{ "type": "text", "bind": "status.message" }],
|
|
36
|
+
"right": [{ "type": "text", "text": "mkui v0.2" }]
|
|
37
|
+
},
|
|
38
|
+
"panes": {
|
|
39
|
+
"explorer": {
|
|
40
|
+
"title": "Explorer",
|
|
41
|
+
"widgets": [
|
|
42
|
+
{ "type": "text", "text": "Project files would go here. Drag the Explorer tab out of this frame to tear it off, drop it on another frame's edge to re-dock." }
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"editor": {
|
|
46
|
+
"title": "Editor",
|
|
47
|
+
"widgets": [
|
|
48
|
+
{ "type": "text", "text": "Editor content. This frame holds two tabs — Editor and Preview. Try dragging the Preview tab out." }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"preview": {
|
|
52
|
+
"title": "Preview",
|
|
53
|
+
"widgets": [
|
|
54
|
+
{ "type": "text", "text": "Preview pane." }
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"console": {
|
|
58
|
+
"title": "Console",
|
|
59
|
+
"widgets": [
|
|
60
|
+
{ "type": "text", "text": "Console log stream would go here." },
|
|
61
|
+
{ "type": "button", "label": "Clear", "action": "demo.clearConsole" }
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"inspector": {
|
|
65
|
+
"title": "Inspector",
|
|
66
|
+
"widgets": [
|
|
67
|
+
{ "type": "text", "text": "Inspector panel — a fourth floating frame to demonstrate multiple top-level frames." }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"frames": [
|
|
72
|
+
{
|
|
73
|
+
"id": "left",
|
|
74
|
+
"x": 0.02, "y": 0.03, "w": 0.22, "h": 0.94,
|
|
75
|
+
"layout": { "type": "tabs", "active": 0, "children": ["explorer"] }
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "main",
|
|
79
|
+
"x": 0.26, "y": 0.03, "w": 0.48, "h": 0.60,
|
|
80
|
+
"layout": { "type": "tabs", "active": 0, "children": ["editor", "preview"] }
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "bottom",
|
|
84
|
+
"x": 0.26, "y": 0.65, "w": 0.48, "h": 0.32,
|
|
85
|
+
"layout": { "type": "tabs", "active": 0, "children": ["console"] }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "right",
|
|
89
|
+
"x": 0.76, "y": 0.03, "w": 0.22, "h": 0.55,
|
|
90
|
+
"layout": { "type": "tabs", "active": 0, "children": ["inspector"] }
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>mkui — standalone</title>
|
|
6
|
+
<link rel="stylesheet" href="../../styles/mkui.css" />
|
|
7
|
+
<script type="module" src="../../src/index.js"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<mkui-app config="./app.json"></mkui-app>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// <mkui-app> — top-level shell. Either fetches a JSON config from its
|
|
2
|
+
// `config` attribute, or accepts a config object via setConfig() in
|
|
3
|
+
// library mode. Builds menubar / workspace / statusbar children and hands
|
|
4
|
+
// each of them an App instance.
|
|
5
|
+
//
|
|
6
|
+
// The workspace is where all windows live — as floating frames, not a
|
|
7
|
+
// single docked tree. Docking happens inside each frame.
|
|
8
|
+
|
|
9
|
+
import { App } from "../core.js";
|
|
10
|
+
import "./menubar.js";
|
|
11
|
+
import "./statusbar.js";
|
|
12
|
+
import "./workspace.js";
|
|
13
|
+
|
|
14
|
+
class MkuiApp extends HTMLElement {
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this._app = null;
|
|
18
|
+
this._built = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async connectedCallback() {
|
|
22
|
+
if (this._built) return;
|
|
23
|
+
this._buildShell();
|
|
24
|
+
const url = this.getAttribute("config");
|
|
25
|
+
if (url) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(url);
|
|
28
|
+
const config = await res.json();
|
|
29
|
+
this.setConfig(config);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error("[mkui] failed to load config:", e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_buildShell() {
|
|
37
|
+
this._built = true;
|
|
38
|
+
this._menubar = document.createElement("mkui-menubar");
|
|
39
|
+
this._workspace = document.createElement("mkui-workspace");
|
|
40
|
+
this._statusbar = document.createElement("mkui-statusbar");
|
|
41
|
+
this.appendChild(this._menubar);
|
|
42
|
+
this.appendChild(this._workspace);
|
|
43
|
+
this.appendChild(this._statusbar);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setConfig(config) {
|
|
47
|
+
if (!this._built) this._buildShell();
|
|
48
|
+
this._app = new App(config);
|
|
49
|
+
this._app.mount(this);
|
|
50
|
+
if (config.app?.title) document.title = config.app.title;
|
|
51
|
+
if (config.app?.theme) this.setAttribute("theme", config.app.theme);
|
|
52
|
+
// Built-in window arrangement actions.
|
|
53
|
+
const ws = this._workspace;
|
|
54
|
+
this._app.registerAction("window.tileH", () => ws.arrangeHorizontal());
|
|
55
|
+
this._app.registerAction("window.tileV", () => ws.arrangeVertical());
|
|
56
|
+
this._app.registerAction("window.grid", () => ws.arrangeGrid());
|
|
57
|
+
this._app.registerAction("window.cascade", () => ws.arrangeCascade());
|
|
58
|
+
|
|
59
|
+
this._menubar.setApp(this._app);
|
|
60
|
+
this._workspace.setApp(this._app);
|
|
61
|
+
this._statusbar.setApp(this._app);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get app() { return this._app; }
|
|
65
|
+
get workspace() { return this._workspace; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!customElements.get("mkui-app")) customElements.define("mkui-app", MkuiApp);
|
|
69
|
+
export { MkuiApp };
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// <mkui-frame> + <mkui-pane>
|
|
2
|
+
//
|
|
3
|
+
// A frame is a floating top-level chrome (titlebar, resize handles, body).
|
|
4
|
+
// Its body hosts a normalized layout tree of panes, tab bars, and splitters.
|
|
5
|
+
// Frames never dock into each other directly — only panes do, via tab
|
|
6
|
+
// drag-out / inter-frame drop zones routed by <mkui-workspace>.
|
|
7
|
+
//
|
|
8
|
+
// Pane elements are pooled at the workspace level (stable identity), so
|
|
9
|
+
// moving a pane between frames is a plain appendChild — content state and
|
|
10
|
+
// subscriptions survive the re-dock.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
normalize, listPanes, layout, setSplitRatio, TABBAR_H,
|
|
14
|
+
} from "../layout/tree.js";
|
|
15
|
+
|
|
16
|
+
class MkuiPane extends HTMLElement {
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this._built = false;
|
|
20
|
+
}
|
|
21
|
+
connectedCallback() { if (!this._built) this._build(); }
|
|
22
|
+
_build() {
|
|
23
|
+
this._built = true;
|
|
24
|
+
const content = document.createElement("div");
|
|
25
|
+
content.className = "mkui-pane-content";
|
|
26
|
+
this.appendChild(content);
|
|
27
|
+
this._content = content;
|
|
28
|
+
}
|
|
29
|
+
get contentEl() { return this._content; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class MkuiFrame extends HTMLElement {
|
|
33
|
+
constructor() {
|
|
34
|
+
super();
|
|
35
|
+
this._built = false;
|
|
36
|
+
this._tree = null;
|
|
37
|
+
this._workspace = null;
|
|
38
|
+
this._app = null;
|
|
39
|
+
this._id = null;
|
|
40
|
+
this._explicitTitle = null;
|
|
41
|
+
this._chromeEls = [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
connectedCallback() { if (!this._built) this._build(); }
|
|
45
|
+
|
|
46
|
+
_build() {
|
|
47
|
+
this._built = true;
|
|
48
|
+
|
|
49
|
+
const titlebar = document.createElement("div");
|
|
50
|
+
titlebar.className = "mkui-frame-titlebar";
|
|
51
|
+
const titleSpan = document.createElement("span");
|
|
52
|
+
titleSpan.className = "mkui-frame-title";
|
|
53
|
+
titlebar.appendChild(titleSpan);
|
|
54
|
+
const actions = document.createElement("div");
|
|
55
|
+
actions.className = "mkui-frame-actions";
|
|
56
|
+
|
|
57
|
+
const maxBtn = document.createElement("div");
|
|
58
|
+
maxBtn.className = "mkui-frame-btn mkui-frame-maximize";
|
|
59
|
+
maxBtn.innerHTML = "◻";
|
|
60
|
+
maxBtn.title = "Maximize";
|
|
61
|
+
maxBtn.addEventListener("mousedown", (ev) => ev.stopPropagation());
|
|
62
|
+
maxBtn.addEventListener("click", (ev) => {
|
|
63
|
+
ev.stopPropagation();
|
|
64
|
+
this._workspace?.toggleMaximize(this._id);
|
|
65
|
+
});
|
|
66
|
+
actions.appendChild(maxBtn);
|
|
67
|
+
|
|
68
|
+
const closeBtn = document.createElement("div");
|
|
69
|
+
closeBtn.className = "mkui-frame-btn mkui-frame-close";
|
|
70
|
+
closeBtn.textContent = "\u00d7";
|
|
71
|
+
closeBtn.title = "Close";
|
|
72
|
+
closeBtn.addEventListener("mousedown", (ev) => ev.stopPropagation());
|
|
73
|
+
closeBtn.addEventListener("click", (ev) => {
|
|
74
|
+
ev.stopPropagation();
|
|
75
|
+
this._workspace?.closeFrame(this._id);
|
|
76
|
+
});
|
|
77
|
+
actions.appendChild(closeBtn);
|
|
78
|
+
|
|
79
|
+
titlebar.appendChild(actions);
|
|
80
|
+
|
|
81
|
+
const body = document.createElement("div");
|
|
82
|
+
body.className = "mkui-frame-body";
|
|
83
|
+
|
|
84
|
+
this.appendChild(titlebar);
|
|
85
|
+
this.appendChild(body);
|
|
86
|
+
|
|
87
|
+
for (const dir of ["n", "s", "e", "w", "ne", "nw", "se", "sw"]) {
|
|
88
|
+
const h = document.createElement("div");
|
|
89
|
+
h.className = `mkui-frame-resize mkui-frame-resize-${dir}`;
|
|
90
|
+
h.addEventListener("mousedown", (ev) => {
|
|
91
|
+
this._workspace?._beginFrameResize(ev, this, dir);
|
|
92
|
+
});
|
|
93
|
+
this.appendChild(h);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
titlebar.addEventListener("mousedown", (ev) => {
|
|
97
|
+
if (ev.button !== 0) return;
|
|
98
|
+
if (ev.target.closest(".mkui-frame-actions")) return;
|
|
99
|
+
this._workspace?._beginFrameMove(ev, this);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Raise to the top of z-order on any interaction inside the frame.
|
|
103
|
+
this.addEventListener("mousedown", () => this._workspace?._raiseFrame(this), true);
|
|
104
|
+
|
|
105
|
+
// Re-render the internal layout whenever the body resizes (either from
|
|
106
|
+
// a frame drag/resize or from viewport-driven clamping).
|
|
107
|
+
this._bodyRO = new ResizeObserver(() => this._renderInternal());
|
|
108
|
+
this._bodyRO.observe(body);
|
|
109
|
+
|
|
110
|
+
this._titlebarEl = titlebar;
|
|
111
|
+
this._titleSpan = titleSpan;
|
|
112
|
+
this._bodyEl = body;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setup(workspace, app, spec) {
|
|
116
|
+
this._workspace = workspace;
|
|
117
|
+
this._app = app;
|
|
118
|
+
this._id = spec.id;
|
|
119
|
+
this._explicitTitle = spec.title ?? null;
|
|
120
|
+
this._tree = normalize(spec.layout);
|
|
121
|
+
this._renderInternal();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get id() { return this._id; }
|
|
125
|
+
get bodyEl() { return this._bodyEl; }
|
|
126
|
+
getTree() { return this._tree; }
|
|
127
|
+
|
|
128
|
+
setTree(tree) {
|
|
129
|
+
const normalized = normalize(tree);
|
|
130
|
+
this._tree = normalized;
|
|
131
|
+
this._renderInternal();
|
|
132
|
+
if (this._tree == null) {
|
|
133
|
+
// Frame has no panes left — close it. Safe: closeFrame just unmounts.
|
|
134
|
+
this._workspace?.closeFrame(this._id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_renderInternal() {
|
|
139
|
+
// Tear down chrome (tab bars, splitters) — rebuilt fresh each render.
|
|
140
|
+
for (const el of this._chromeEls) el.remove();
|
|
141
|
+
this._chromeEls = [];
|
|
142
|
+
|
|
143
|
+
// Park any pane element no longer in this frame's tree. Park them in
|
|
144
|
+
// the workspace pool rather than destroying them so that moving a pane
|
|
145
|
+
// between frames preserves its state.
|
|
146
|
+
const wanted = new Set(this._tree ? listPanes(this._tree) : []);
|
|
147
|
+
for (const child of [...this._bodyEl.children]) {
|
|
148
|
+
if (child.tagName === "MKUI-PANE") {
|
|
149
|
+
const id = child.getAttribute("data-id");
|
|
150
|
+
if (!wanted.has(id)) this._workspace?._parkPane(child);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (this._tree == null) {
|
|
155
|
+
this._titleSpan.textContent = this._explicitTitle ?? "";
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const bw = this._bodyEl.clientWidth;
|
|
160
|
+
const bh = this._bodyEl.clientHeight;
|
|
161
|
+
const bodyRect = { x: 0, y: 0, w: bw, h: bh };
|
|
162
|
+
const { panes, tabBars, splitters } = layout(this._tree, bodyRect);
|
|
163
|
+
|
|
164
|
+
// Attach + position each pane that should live in this frame.
|
|
165
|
+
for (const [id, info] of panes) {
|
|
166
|
+
const el = this._workspace._ensurePaneEl(id);
|
|
167
|
+
if (el.parentElement !== this._bodyEl) this._bodyEl.appendChild(el);
|
|
168
|
+
Object.assign(el.style, {
|
|
169
|
+
left: info.rect.x + "px",
|
|
170
|
+
top: info.rect.y + "px",
|
|
171
|
+
width: info.rect.w + "px",
|
|
172
|
+
height: info.rect.h + "px",
|
|
173
|
+
display: info.visible ? "" : "none",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Tab bars.
|
|
178
|
+
for (const { tabGroup, rect } of tabBars) {
|
|
179
|
+
const bar = this._renderTabBar(tabGroup, rect);
|
|
180
|
+
this._bodyEl.appendChild(bar);
|
|
181
|
+
this._chromeEls.push(bar);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Splitters.
|
|
185
|
+
for (const sp of splitters) {
|
|
186
|
+
const h = document.createElement("div");
|
|
187
|
+
h.className = "mkui-splitter " + (sp.dir === "h" ? "mkui-splitter-h" : "mkui-splitter-v");
|
|
188
|
+
Object.assign(h.style, {
|
|
189
|
+
left: sp.rect.x + "px",
|
|
190
|
+
top: sp.rect.y + "px",
|
|
191
|
+
width: sp.rect.w + "px",
|
|
192
|
+
height: sp.rect.h + "px",
|
|
193
|
+
});
|
|
194
|
+
h.addEventListener("mousedown", (ev) => this._beginSplitterDrag(ev, sp));
|
|
195
|
+
this._bodyEl.appendChild(h);
|
|
196
|
+
this._chromeEls.push(h);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this._updateTitle();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_updateTitle() {
|
|
203
|
+
if (this._explicitTitle) {
|
|
204
|
+
this._titleSpan.textContent = this._explicitTitle;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// Fall back to the title of the first pane in traversal order.
|
|
208
|
+
const ids = this._tree ? listPanes(this._tree) : [];
|
|
209
|
+
if (ids.length === 0) { this._titleSpan.textContent = ""; return; }
|
|
210
|
+
const spec = this._workspace?.getPaneSpec(ids[0]);
|
|
211
|
+
this._titleSpan.textContent = spec?.title ?? ids[0];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_renderTabBar(tabGroup, rect) {
|
|
215
|
+
const bar = document.createElement("div");
|
|
216
|
+
bar.className = "mkui-tabbar";
|
|
217
|
+
Object.assign(bar.style, {
|
|
218
|
+
position: "absolute",
|
|
219
|
+
left: rect.x + "px", top: rect.y + "px",
|
|
220
|
+
width: rect.w + "px", height: rect.h + "px",
|
|
221
|
+
});
|
|
222
|
+
for (let i = 0; i < tabGroup.children.length; i++) {
|
|
223
|
+
const id = tabGroup.children[i];
|
|
224
|
+
const spec = this._workspace?.getPaneSpec(id);
|
|
225
|
+
const tab = document.createElement("div");
|
|
226
|
+
tab.className = "mkui-tab" + (i === tabGroup.active ? " active" : "");
|
|
227
|
+
tab.textContent = spec?.title ?? id;
|
|
228
|
+
tab.addEventListener("mousedown", (ev) => {
|
|
229
|
+
if (ev.button !== 0) return;
|
|
230
|
+
ev.stopPropagation();
|
|
231
|
+
this._workspace?._beginPaneDrag(ev, this, id, tabGroup, bar);
|
|
232
|
+
});
|
|
233
|
+
bar.appendChild(tab);
|
|
234
|
+
}
|
|
235
|
+
return bar;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_beginSplitterDrag(ev, sp) {
|
|
239
|
+
ev.preventDefault();
|
|
240
|
+
ev.stopPropagation();
|
|
241
|
+
const horiz = sp.dir === "h";
|
|
242
|
+
const startPos = horiz ? ev.clientX : ev.clientY;
|
|
243
|
+
const startRatio = sp.splitNode.ratios[sp.index];
|
|
244
|
+
const dim = sp.parentDim;
|
|
245
|
+
const move = (e) => {
|
|
246
|
+
const cur = horiz ? e.clientX : e.clientY;
|
|
247
|
+
setSplitRatio(sp.splitNode, sp.index, startRatio + (cur - startPos) / dim);
|
|
248
|
+
this._renderInternal();
|
|
249
|
+
};
|
|
250
|
+
const up = () => {
|
|
251
|
+
window.removeEventListener("mousemove", move);
|
|
252
|
+
window.removeEventListener("mouseup", up);
|
|
253
|
+
};
|
|
254
|
+
window.addEventListener("mousemove", move);
|
|
255
|
+
window.addEventListener("mouseup", up);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!customElements.get("mkui-pane")) customElements.define("mkui-pane", MkuiPane);
|
|
260
|
+
if (!customElements.get("mkui-frame")) customElements.define("mkui-frame", MkuiFrame);
|
|
261
|
+
|
|
262
|
+
export { MkuiPane, MkuiFrame };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// <mkui-menubar> — top menubar with dropdown popups. Config:
|
|
2
|
+
//
|
|
3
|
+
// [[menubar]]
|
|
4
|
+
// label = "File"
|
|
5
|
+
// items = [
|
|
6
|
+
// { label = "New", action = "window.new" },
|
|
7
|
+
// { sep = true },
|
|
8
|
+
// { label = "Quit", action = "app.quit" },
|
|
9
|
+
// ]
|
|
10
|
+
|
|
11
|
+
class MkuiMenubar extends HTMLElement {
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this._app = null;
|
|
15
|
+
this._openPopup = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setApp(app) {
|
|
19
|
+
this._app = app;
|
|
20
|
+
this._render();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_render() {
|
|
24
|
+
this.innerHTML = "";
|
|
25
|
+
const items = this._app?.config?.menubar ?? [];
|
|
26
|
+
for (const menu of items) {
|
|
27
|
+
const el = document.createElement("div");
|
|
28
|
+
el.className = "mkui-menu";
|
|
29
|
+
el.textContent = menu.label;
|
|
30
|
+
el.addEventListener("mousedown", (ev) => {
|
|
31
|
+
ev.stopPropagation();
|
|
32
|
+
this._toggleMenu(el, menu);
|
|
33
|
+
});
|
|
34
|
+
this.appendChild(el);
|
|
35
|
+
}
|
|
36
|
+
document.addEventListener("mousedown", () => this._closePopup());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_toggleMenu(anchor, menu) {
|
|
40
|
+
if (this._openPopup) {
|
|
41
|
+
this._closePopup();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const popup = document.createElement("div");
|
|
45
|
+
popup.className = "mkui-menu-popup";
|
|
46
|
+
popup.style.left = anchor.offsetLeft + "px";
|
|
47
|
+
for (const item of menu.items ?? []) {
|
|
48
|
+
if (item.sep) {
|
|
49
|
+
const s = document.createElement("div");
|
|
50
|
+
s.className = "mkui-menu-sep";
|
|
51
|
+
popup.appendChild(s);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const it = document.createElement("div");
|
|
55
|
+
it.className = "mkui-menu-item";
|
|
56
|
+
it.textContent = item.label;
|
|
57
|
+
it.addEventListener("mousedown", (ev) => {
|
|
58
|
+
ev.stopPropagation();
|
|
59
|
+
this._closePopup();
|
|
60
|
+
if (item.action) this._app.fireAction(item.action, item.args);
|
|
61
|
+
});
|
|
62
|
+
popup.appendChild(it);
|
|
63
|
+
}
|
|
64
|
+
anchor.classList.add("open");
|
|
65
|
+
this.appendChild(popup);
|
|
66
|
+
this._openPopup = { popup, anchor };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_closePopup() {
|
|
70
|
+
if (!this._openPopup) return;
|
|
71
|
+
this._openPopup.popup.remove();
|
|
72
|
+
this._openPopup.anchor.classList.remove("open");
|
|
73
|
+
this._openPopup = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!customElements.get("mkui-menubar")) customElements.define("mkui-menubar", MkuiMenubar);
|
|
78
|
+
export { MkuiMenubar };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// <mkui-statusbar> — bottom strip with widget slots on the left and right.
|
|
2
|
+
// Config:
|
|
3
|
+
// [statusbar]
|
|
4
|
+
// left = [{ type = "text", bind = "status.message" }]
|
|
5
|
+
// right = [{ type = "text", text = "v0.1" }]
|
|
6
|
+
|
|
7
|
+
import { getWidget } from "../core.js";
|
|
8
|
+
|
|
9
|
+
class MkuiStatusbar extends HTMLElement {
|
|
10
|
+
setApp(app) {
|
|
11
|
+
this._app = app;
|
|
12
|
+
this._render();
|
|
13
|
+
}
|
|
14
|
+
_render() {
|
|
15
|
+
this.innerHTML = "";
|
|
16
|
+
const cfg = this._app?.config?.statusbar ?? {};
|
|
17
|
+
const left = document.createElement("div");
|
|
18
|
+
left.className = "mkui-status-side";
|
|
19
|
+
const right = document.createElement("div");
|
|
20
|
+
right.className = "mkui-status-side";
|
|
21
|
+
for (const w of cfg.left ?? []) {
|
|
22
|
+
const fn = getWidget(w.type);
|
|
23
|
+
if (fn) fn(w, this._app, left);
|
|
24
|
+
}
|
|
25
|
+
for (const w of cfg.right ?? []) {
|
|
26
|
+
const fn = getWidget(w.type);
|
|
27
|
+
if (fn) fn(w, this._app, right);
|
|
28
|
+
}
|
|
29
|
+
this.appendChild(left);
|
|
30
|
+
this.appendChild(right);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!customElements.get("mkui-statusbar")) customElements.define("mkui-statusbar", MkuiStatusbar);
|
|
35
|
+
export { MkuiStatusbar };
|