gui/docs/index.html
2026-08-25 01:47:43 +07:00

432 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>X11 GUI Library — Documentation</title>
<style>
:root{--bg:#202020;--bg2:#2b2b2b;--bg3:#383838;--tx:#fff;--tx2:#9d9d9d;--ac:#0078d4;--ach:#2488e0;--bd:#3b3b3b;--r:8px}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--tx);display:flex}
::-webkit-scrollbar{width:8px}::-webkit-scrollbar-thumb{background:var(--bg3);border-radius:4px}
.sb{width:260px;position:fixed;inset:0 auto 0 0;background:var(--bg2);border-right:1px solid var(--bd);padding:16px 12px;overflow-y:auto}
.logo{font-size:17px;font-weight:600;padding:6px 12px 18px;display:flex;gap:10px;align-items:center}
.logo .sq{width:22px;height:22px;background:var(--ac);border-radius:5px}
#q{width:100%;background:var(--bg);border:1px solid var(--bd);border-radius:6px;color:var(--tx);padding:8px 10px;font-size:13px;margin-bottom:12px;outline:none}
#q:focus{border-color:var(--ac)}
.nv{display:block;width:100%;text-align:left;background:none;border:none;color:var(--tx2);padding:10px 12px;border-radius:6px;font-size:14px;cursor:pointer;margin-bottom:2px;position:relative}
.nv:hover{background:var(--bg3);color:var(--tx)}
.nv.on{background:var(--bg3);color:var(--tx)}
.nv.on::before{content:'';position:absolute;left:0;top:8px;bottom:8px;width:3px;background:var(--ac);border-radius:2px}
main{margin-left:260px;flex:1;padding:32px 40px;max-width:1100px}
h1{font-size:28px;font-weight:600;margin-bottom:6px}
h2{font-size:20px;font-weight:600;margin:26px 0 10px}
h3{font-size:15px;font-weight:600;margin:18px 0 8px}
p,li{color:var(--tx2);font-size:14px;line-height:1.7}
ul{padding-left:20px;margin:8px 0}
.card{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:20px;margin:14px 0}
table{width:100%;border-collapse:collapse;margin:12px 0;font-size:13px}
th,td{padding:8px 10px;text-align:left;border-bottom:1px solid var(--bd);color:var(--tx2);vertical-align:top}
th{color:var(--tx);background:var(--bg3)}
code{font-family:'Cascadia Code',Consolas,monospace;font-size:12.5px;color:#4cc2ff}
pre{background:#1a1a1a;border:1px solid var(--bd);border-radius:var(--r);padding:14px;overflow-x:auto;margin:10px 0}
pre code{color:#e8e8e8;line-height:1.6}
.cb{position:relative}
.cp{position:absolute;top:8px;right:8px;background:var(--bg3);border:1px solid var(--bd);color:var(--tx2);border-radius:5px;padding:3px 10px;font-size:11px;cursor:pointer}
.cp:hover{background:var(--ac);color:#fff}
.page{display:none}.page.on{display:block}
.badge{display:inline-block;background:var(--ac);color:#fff;border-radius:4px;padding:2px 8px;font-size:11px;margin-right:6px}
.tip{border-left:3px solid var(--ac);background:var(--bg2);padding:12px 16px;border-radius:0 6px 6px 0;margin:12px 0}
.warn{border-left:3px solid #f59e0b;background:var(--bg2);padding:12px 16px;border-radius:0 6px 6px 0;margin:12px 0}
</style>
</head>
<body>
<aside class="sb">
<div class="logo"><div class="sq"></div>X11 GUI Library</div>
<input id="q" placeholder="Search...">
<button class="nv on" data-p="start"> Getting Started</button>
<button class="nv" data-p="window">🪟 Window &amp; Pages</button>
<button class="nv" data-p="widgets">🧩 Widgets</button>
<button class="nv" data-p="themes">🎨 Themes &amp; Customization</button>
<button class="nv" data-p="custom">🛠 Custom Widgets</button>
<button class="nv" data-p="events">📡 Events</button>
<button class="nv" data-p="anim">✨ Animations</button>
<button class="nv" data-p="debug">🐞 Debug &amp; Performance</button>
<button class="nv" data-p="api">📚 API Reference</button>
</aside>
<main>
<!-- ============ START ============ -->
<section id="p-start" class="page on">
<h1>Getting Started</h1>
<p>Lightweight X11 GUI library for C++20 with software rendering, dirty-rectangle partial updates, TTF fonts (FreeType), two built-in themes (WIN11 / TERMINAL) and smooth animations.</p>
<div class="card">
<h3>Features</h3>
<ul>
<li>Partial redraw (dirty rectangles) — only changed regions repaint</li>
<li>Double buffering — no flicker</li>
<li>TTF fonts via FreeType with cached 1-bit masks (fast text)</li>
<li>Two themes: <code>WIN11</code> and <code>TERMINAL</code>, fully customizable</li>
<li>Animations: hover, press, push-down, check, dropdown</li>
<li>EventBus + per-widget callbacks</li>
<li>Debug overlay, dirty-rect visualization, click markers (F1–F6)</li>
</ul>
</div>
<h2>Build</h2>
<pre><code>find_package(X11 REQUIRED)
find_package(Freetype REQUIRED)
add_executable(host
main.cpp
libs/gui/core/event_bus.cpp
libs/gui/core/raw_picture.cpp
libs/gui/core/font_manager.cpp
libs/gui/core/theme.cpp
libs/gui/widgets/widget.cpp
libs/gui/widgets/draw_basic.cpp
libs/gui/widgets/draw_toggles.cpp
libs/gui/widgets/draw_lists.cpp
libs/gui/window.cpp
libs/gui/demo.cpp
)
target_include_directories(host PRIVATE libs/gui ${X11_INCLUDE_DIR})
target_link_libraries(host PRIVATE ${X11_LIBRARIES} Freetype::Freetype)</code></pre>
<h2>Quick start</h2>
<pre><code>#include "gui.h"
int main() {
gui::set_theme(gui::ThemeStyle::WIN11);
gui::window win("My App");
win.set_font("SGr-Iosevka-Regular.ttc");
win.set_default_icon();
win.configur_page("main", [](gui::page&amp; p) {
gui::WidgetData b;
b.id = "btn"; b.text = "Hello";
b.x = 100; b.y = 100; b.width = 300; b.height = 70;
p.Button(b);
}, true);
gui::EventBus::get().on("button.click", [](const gui::EventData&amp; e) {
std::cout &lt;&lt; "clicked: " &lt;&lt; e.source_id &lt;&lt; "\n";
});
while (win.running()) win.update();
}</code></pre>
<div class="tip">Put <code>SGr-Iosevka-Regular.ttc</code> (or any TTF/TTC) next to the binary. If not found, a fallback X11 core font is used.</div>
</section>
<!-- ============ WINDOW ============ -->
<section id="p-window" class="page">
<h1>Window &amp; Pages</h1>
<h2>gui::window</h2>
<table>
<tr><th>Method</th><th>Description</th></tr>
<tr><td><code>window(title)</code></td><td>Create X11 window (1600×900), backbuffer, load font</td></tr>
<tr><td><code>update()</code></td><td>Process events + render one frame (call in a loop)</td></tr>
<tr><td><code>running()</code></td><td>false after WM_DELETE (close button)</td></tr>
<tr><td><code>set_font(path)</code></td><td>Load TTF/TTC font</td></tr>
<tr><td><code>set_font_size(px)</code></td><td>Global font size (default 32)</td></tr>
<tr><td><code>set_icon(path)</code> / <code>set_icon(pic)</code> / <code>set_default_icon()</code></td><td>Window icon (_NET_WM_ICON)</td></tr>
<tr><td><code>set_max_fps(n)</code></td><td>Frame cap (default 240)</td></tr>
<tr><td><code>on_key</code></td><td>std::function&lt;void(KeySym)&gt; — custom hotkeys (F6+)</td></tr>
</table>
<h2>Pages &amp; tabs</h2>
<pre><code>// tabs (left column)
gui::WidgetData tab; tab.id = "main"; tab.text = "main";
tab.on_window_click = [&amp;win](gui::window&amp; w){ w.openPAGE("main"); };
win.addTAB_BUTTON(tab);
// pages
win.addPAGE("main");
win.configur_page("main", [](gui::page&amp; p){ /* fill widgets */ }, true);
win.openPAGE("main");</code></pre>
<div class="tip">Only the <b>active page</b> is ticked and rendered — inactive pages cost zero CPU.</div>
<h2>Finding widgets</h2>
<pre><code>gui::Widget* w = win.find("btn"); // search all pages
if (w) { w->setValue(0.7f); win.mark_dirty(100,100,300,70); }</code></pre>
</section>
<!-- ============ WIDGETS ============ -->
<section id="p-widgets" class="page">
<h1>Widgets</h1>
<h2>WidgetData fields</h2>
<table>
<tr><th>Field</th><th>Default</th><th>Meaning</th></tr>
<tr><td><code>id, text</code></td><td>""</td><td>identifier / label</td></tr>
<tr><td><code>x,y,width,height</code></td><td>0,0,200,60</td><td>geometry (2× scale)</td></tr>
<tr><td><code>bg_color, fg_color, frame_color</code></td><td>0</td><td>0 = take from theme, else override</td></tr>
<tr><td><code>font_size</code></td><td>0</td><td>0 = theme size, else per-widget</td></tr>
<tr><td><code>primary</code></td><td>false</td><td>accent-colored button</td></tr>
<tr><td><code>checked</code></td><td>false</td><td>checkbox / radio state</td></tr>
<tr><td><code>value, target_value</code></td><td>0</td><td>slider / progress (0..1)</td></tr>
<tr><td><code>items</code></td><td>[]</td><td>listbox / dropdown entries</td></tr>
<tr><td><code>picture</code></td><td>null</td><td>RawPicture for sprite / raw widgets</td></tr>
<tr><td><code>on_change / on_page_click / on_window_click</code></td><td>null</td><td>callbacks</td></tr>
</table>
<h2>Factory methods (on page or any container)</h2>
<pre><code>p.Button(d); p.Label(d); p.Input(d); p.Checkbox(d); p.Radiobutton(d);
p.Slider(d); p.Progressbar(d); p.Listbox(d); p.Dropdown(d);
p.TreeView(d); p.TreeNode(d); p.ScrollView(d);
p.RawPictureWidget(d); p.SpriteButton(d);</code></pre>
<h2>Examples</h2>
<pre><code>// accent button
gui::WidgetData b; b.id="go"; b.text="Run"; b.primary=true;
b.x=100; b.y=100; b.width=300; b.height=70;
p.Button(b);
// input field (click to focus, bottom line + blinking caret)
gui::WidgetData i; i.id="name"; i.x=100; i.y=200; i.width=500; i.height=68;
p.Input(i);
// slider drives progress
gui::WidgetData s; s.id="vol"; s.x=100; s.y=300; s.width=600; s.height=48; s.value=0.4f;
p.Slider(s);
gui::WidgetData pr; pr.id="prog"; pr.x=100; pr.y=380; pr.width=600; pr.height=44;
p.Progressbar(pr);</code></pre>
<h2>Nested containers</h2>
<pre><code>auto&amp; sv = p.ScrollView(d); // returns Widget&amp;
sv.Button(inner); // children live inside
auto&amp; tv = p.TreeView(d);
auto&amp; n1 = tv.TreeNode(n); // node
n1.Label(child); // leaf inside node</code></pre>
</section>
<!-- ============ THEMES ============ -->
<section id="p-themes" class="page">
<h1>Themes &amp; Customization</h1>
<h2>Switch built-in themes</h2>
<pre><code>gui::set_theme(gui::ThemeStyle::WIN11); // rounded, blue accent
gui::set_theme(gui::ThemeStyle::TERMINAL); // square, green accent
// hotkey F6 toggles at runtime (see demo)</code></pre>
<h2>Theme struct (all tunable)</h2>
<pre><code>struct Theme {
ThemeStyle style;
int radius, radius_sm, font_size; // 12/8/32 (win11), 0/0/32 (term)
uint32_t window_bg, panel_bg, text, text_dim;
uint32_t btn, btn_h, btn_p, border, border_h; // button + hover + press
uint32_t accent, accent_h, accent_p, on_accent;
uint32_t input_bg, focus;
uint32_t track, prog_bg;
uint32_t list_bg, list_h, list_sel, list_sel_text;
uint32_t tab_bg, tab_h, tab_active;
};
const Theme&amp; theme(); // current</code></pre>
<h2>Per-widget override (beats theme)</h2>
<pre><code>gui::WidgetData d;
d.bg_color = 0x332211; // non-zero overrides theme
d.fg_color = 0xFFCC00;
d.frame_color = 0x00FF88;
d.font_size = 40; // per-widget size</code></pre>
<div class="tip">Rule: <code>0</code> = use theme. Any other value = your override. This is the <code>C(override, theme)</code> helper in <code>widget.h</code>.</div>
<h2>Where theme values live</h2>
<p>Edit <code>libs/gui/core/theme.cpp</code> — the two static structs <code>t_win11</code> / <code>t_term</code>. Field order matches the struct above (e.g. the pair <code>input_bg, focus</code> is the 14th/15th value).</p>
</section>
<!-- ============ CUSTOM ============ -->
<section id="p-custom" class="page">
<h1>Custom Widgets</h1>
<p>Three levels of extension, from easy to full.</p>
<h2>1. Restyle an existing widget (no code change)</h2>
<pre><code>gui::WidgetData d;
d.type = gui::WidgetType::BUTTON; // or set via factory
d.bg_color = 0x402020; d.frame_color = 0xFF4444; d.font_size = 28;
p.Button(d);</code></pre>
<h2>2. Compose with RawPicture (sprite button)</h2>
<pre><code>auto pic = std::make_shared&lt;gui::RawPicture&gt;(300, 100, 32);
pic-&gt;clear(0x202020);
pic-&gt;fillRoundRect(0,0,300,100,16,0x0078D4);
pic-&gt;drawText(win.display(), 20, 30, "Custom", 0xFFFFFF, 1);
gui::WidgetData d; d.picture = pic; d.width=300; d.height=100;
p.SpriteButton(d); // still clickable, emits button.click</code></pre>
<h2>3. Add a brand-new widget type</h2>
<ol>
<li><code>widget.h</code>: add enum value <code>WidgetType::MY_WIDGET</code></li>
<li><code>widget.h</code>: add factory decl <code>Widget&amp; MyWidget(WidgetData);</code></li>
<li><code>widget.cpp</code>: implement factory (<code>a.type = ...; return add_widget(...)</code>)</li>
<li>draw: add a case in <code>Widget::draw</code> dispatch → your <code>wdraw::my(...)</code></li>
<li>input: add a case in <code>Widget::handle_press</code> (return 1 consumed / 2 capture)</li>
</ol>
<pre><code>// widget.cpp factory
Widget&amp; combat::MyWidget(WidgetData a){ a.type=WidgetType::MY_WIDGET; return add_widget(std::move(a)); }
// draw dispatch (Widget::draw)
case WidgetType::MY_WIDGET: wdraw::my(d, win, gc, *this, ax, ay); break;
// your renderer (any draw_*.cpp)
void wdraw::my(Display* d, Drawable win, GC gc, const Widget&amp; w, int ax, int ay) {
const auto&amp; t = theme();
XSetForeground(d, gc, t.accent);
x_fill_round(d, win, gc, ax, ay, w.data.width, w.data.height, t.radius);
gui_text(d, win, gc, ax+12, gui_base_c(ay, w.data.height, t.font_size),
w.data.text, t.on_accent, t.font_size);
}</code></pre>
<div class="warn">Declare your <code>wdraw::my</code> in <code>widget.h</code> and, if it needs protected <code>widgets</code>, add it as <code>friend</code> of <code>combat</code> (like basic/toggles/lists).</div>
</section>
<!-- ============ EVENTS ============ -->
<section id="p-events" class="page">
<h1>Events</h1>
<h2>EventBus</h2>
<pre><code>int tok = gui::EventBus::get().on("button.click", [](const gui::EventData&amp; e){ ... });
gui::EventBus::get().off(tok);
gui::EventBus::get().emit("my.event", {"id","text",0.5f,1,true});</code></pre>
<h2>Built-in events</h2>
<table>
<tr><th>Event</th><th>Data</th></tr>
<tr><td><code>button.click</code></td><td>source_id, text</td></tr>
<tr><td><code>checkbox.change</code></td><td>source_id, flag</td></tr>
<tr><td><code>radio.change</code></td><td>source_id, flag=true</td></tr>
<tr><td><code>slider.change</code></td><td>source_id, value 0..1</td></tr>
<tr><td><code>list.select</code></td><td>source_id, text, index</td></tr>
<tr><td><code>dropdown.select</code></td><td>source_id, text, index</td></tr>
<tr><td><code>tree.toggle</code></td><td>source_id, flag=expanded</td></tr>
<tr><td><code>input.submit</code></td><td>source_id, text (Enter)</td></tr>
<tr><td><code>page.open</code></td><td>source_id = page name</td></tr>
</table>
<h2>Per-widget callbacks (alternative)</h2>
<pre><code>d.on_change = []{ ... }; // value/state changed
d.on_page_click = [](gui::page&amp; p){ ... }; // button clicked
d.on_window_click= [](gui::window&amp; w){ ... };</code></pre>
<h2>Global logger (debug)</h2>
<pre><code>gui::EventBus::get().set_global_logger([](const std::string&amp; e, const gui::EventData&amp; d){
std::cout &lt;&lt; "[LOG] " &lt;&lt; e &lt;&lt; "\n";
});</code></pre>
</section>
<!-- ============ ANIM ============ -->
<section id="p-anim" class="page">
<h1>Animations</h1>
<p>All animation state lives in <code>struct Widget</code> as 0..1 floats, advanced in <code>Widget::tick(dt)</code> and applied in <code>Widget::draw()</code>.</p>
<table>
<tr><th>Field</th><th>Drives</th><th>Where applied</th></tr>
<tr><td><code>hover_a</code></td><td>hover highlight</td><td>draw_basic / toggles color mix</td></tr>
<tr><td><code>press_a</code></td><td>press darkening</td><td>draw_basic color mix</td></tr>
<tr><td><code>push_a</code></td><td>push-down offset (6px)</td><td><code>Widget::draw</code>: <code>ay += ease(push_a)*6</code></td></tr>
<tr><td><code>check_a</code></td><td>checkmark / radio dot reveal</td><td>draw_toggles</td></tr>
<tr><td><code>anim</code></td><td>dropdown open/close</td><td>draw_lists popup height</td></tr>
<tr><td><code>cursor_timer / cursor_visible</code></td><td>input caret blink (0.5s)</td><td>draw_basic</td></tr>
</table>
<h2>Tune speed</h2>
<pre><code>// Widget::tick — the speed constants
hover_a = approach(hover_a, hovered?1:0, dt, 10); // higher = snappier
push_a = approach(push_a, pressed?1:0, dt, 18);
check_a = approach(check_a, checked?1:0, dt, 10);</code></pre>
<h2>Disable push-down (e.g. for slider)</h2>
<pre><code>// Widget::draw
int ay = data.y + oy + (data.type == WidgetType::SLIDER ? 0 : (int)(ease(push_a)*6));</code></pre>
<h2>Remove color flicker (design rule)</h2>
<div class="tip">Colors fed into <code>x_fill_round</code>/<code>x_draw_round</code>/<code>XDrawArc</code> must NOT depend on animated <code>hover_a</code>/<code>press_a</code>. Keep animation in safe places: checkmark lines, radio dot, slider thumb position, push-down offset. This is why checkbox/radio use static border colors.</div>
</section>
<!-- ============ DEBUG ============ -->
<section id="p-debug" class="page">
<h1>Debug &amp; Performance</h1>
<h2>Hotkeys</h2>
<table>
<tr><th>Key</th><th>Action</th></tr>
<tr><td>F1</td><td>Debug overlay (FPS, widgets, partial updates, mouse, event log)</td></tr>
<tr><td>F2</td><td>Dirty-rect visualization (XOR flash of repainted regions)</td></tr>
<tr><td>F3</td><td>Click markers (red cross at click point)</td></tr>
<tr><td>F4</td><td>Widget bounds (magenta outlines)</td></tr>
<tr><td>F5</td><td>Console event logging</td></tr>
<tr><td>F6</td><td>Toggle theme WIN11 ↔ TERMINAL (via <code>on_key</code>)</td></tr>
</table>
<h2>Programmatic access</h2>
<pre><code>win.debug().show_debug_overlay = true;
win.debug().show_dirty_rects = true;
win.debug().max_log_size = 30;
std::cout &lt;&lt; win.debug().fps &lt;&lt; "\n";
win.mark_dirty(x,y,w,h); // invalidate a region
win.mark_all_dirty(); // full repaint</code></pre>
<h2>Why it's fast</h2>
<ul>
<li><b>Dirty rectangles</b> — only changed regions redraw; <code>Partial updates</code> counter shows it</li>
<li><b>Only active page</b> is ticked/rendered</li>
<li><b>Text cache</b> — strings rendered once to 1-bit masks, drawn with one <code>XFillRectangle</code> (no XGetImage)</li>
<li><b>Overlay throttle</b> — debug panel refreshes at 10 Hz</li>
<li><b>Idle sleep</b> — <code>usleep(10ms)</code> when nothing is dirty → ~0% CPU at rest</li>
<li><b>Frame cap</b> — <code>set_max_fps()</code>, default 240</li>
</ul>
<div class="warn">F2 flashes only visualize <i>user</i> dirty rects (snapshot taken before flash/marker/overlay rects) — no feedback loop, FPS stays high.</div>
</section>
<!-- ============ API ============ -->
<section id="p-api" class="page">
<h1>API Reference</h1>
<h2>gui::window</h2>
<pre><code>window(title) / ~window()
update() running() display()
addTAB_BUTTON(d) addPAGE(n) configur_page(n, fn, active) openPAGE(n)
find(id) set_focus(w)
set_font(p) set_font_size(px) set_icon(p|pic) set_default_icon() set_max_fps(n)
debug() mark_dirty(x,y,w,h) mark_all_dirty()
std::function&lt;void(KeySym)&gt; on_key;</code></pre>
<h2>gui::combat (container base: page, Widget)</h2>
<pre><code>Button/Label/Input/Checkbox/Radiobutton/Slider/Progressbar/
Listbox/Dropdown/TreeView/TreeNode/ScrollView/RawPictureWidget/SpriteButton
visit(fn) find(id) find_at(x,y) count_widgets()
tick(dt) render_all(...) render_dirty(...) mark_all_dirty(...)
process_press(...) process_wheel(...)</code></pre>
<h2>gui::Widget</h2>
<pre><code>WidgetData data;
float hover_a, press_a, check_a, push_a, anim, phase;
bool hovered, pressed, focused, cursor_visible, open_target;
float cursor_timer;
setValue(v) getValue()
tick(dt) draw(...) draw_popup(...)
handle_press/drag/release/wheel(...)
contains(...) content_height() mark_dirty(...)</code></pre>
<h2>gui::RawPicture</h2>
<pre><code>create(w,h,bits) clear(c) setPixel/getPixel/blendPixel
drawLine/drawRect/fillRect/fillRoundRect
drawCircle/fillCircle/fillTriangle/fillPolygon
drawText(dpy,x,y,t,c,scale) blit(src,dx,dy)
load(path) // BMP 24/32, PPM P6
present(dpy,win,gc,dx,dy)</code></pre>
<h2>gui::FontManager</h2>
<pre><code>FontManager::get()
load(path) available() set_default_size(px) default_size()
ascent/descent/line_height/text_width(size)
draw(RawPicture&amp;,...) draw(Display*,Drawable,GC,...)</code></pre>
<h2>gui::EventBus / gui::DirtyRegion / gui::Theme</h2>
<pre><code>EventBus::get().on/off/emit/set_global_logger
DirtyRegion: add/mark_all/is_dirty/intersects/get/clear
theme() set_theme(ThemeStyle)</code></pre>
</section>
</main>
<script>
function go(id){
document.querySelectorAll('.page').forEach(p=>p.classList.remove('on'));
document.querySelectorAll('.nv').forEach(n=>n.classList.remove('on'));
document.getElementById('p-'+id).classList.add('on');
document.querySelector('[data-p="'+id+'"]').classList.add('on');
window.scrollTo(0,0);
}
document.querySelectorAll('.nv').forEach(n=>n.onclick=()=>go(n.dataset.p));
// wrap every <pre> with copy button
document.querySelectorAll('pre').forEach(pre=>{
const wrap=document.createElement('div'); wrap.className='cb';
pre.parentNode.insertBefore(wrap,pre); wrap.appendChild(pre);
const b=document.createElement('button'); b.className='cp'; b.textContent='Copy';
b.onclick=()=>{ navigator.clipboard.writeText(pre.innerText).then(()=>{
b.textContent='Copied!'; setTimeout(()=>b.textContent='Copy',1200);
});};
wrap.appendChild(b);
});
// sidebar search
document.getElementById('q').addEventListener('input',e=>{
const t=e.target.value.toLowerCase();
document.querySelectorAll('.nv').forEach(n=>{
n.style.display = n.textContent.toLowerCase().includes(t) ? 'block' : 'none';
});
});
</script>
</body>
</html>