From 98fecb4cb7d7ef188d20b3cb94422b8eeb08bca4 Mon Sep 17 00:00:00 2001 From: Tzvetan Mikov Date: Sat, 8 Nov 2025 21:57:43 -0800 Subject: [PATCH 1/5] Add GitHub Actions CI workflow for multi-platform builds Add CI workflow that builds on Ubuntu and macOS in both Debug and Release configurations (2x2 matrix = 4 jobs). Key features: - Hermes build caching per OS (saves 5-10 min on cache hit) - Cache optimization: removes .git directory to save ~100MB - Platform-specific dependency installation - Explicit Clang compiler selection on Ubuntu - Uploads all example executables as artifacts - Runs on push to master, PRs, and manual dispatch Hermes is always built in Release mode and shared between Debug/Release project builds on the same OS. --- .github/workflows/build.yml | 113 ++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ce5aef9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,113 @@ +name: Build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + workflow_dispatch: + +env: + HERMES_VERSION: 80359d48dbf0a108031d69c8a22bad180cfb4df3 + +jobs: + build: + name: ${{ matrix.os }} (${{ matrix.build_type }}) + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + build_type: [Debug, Release] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Ubuntu dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libx11-dev \ + libxi-dev \ + libxcursor-dev \ + libgl1-mesa-dev \ + libicu-dev \ + clang \ + cmake \ + ninja-build \ + nodejs \ + npm + + - name: Install macOS dependencies + if: runner.os == 'macOS' + run: | + brew install cmake ninja node + + - name: Cache Hermes build + id: hermes-cache + uses: actions/cache@v4 + with: + path: | + hermes-src + hermes-build + key: ${{ runner.os }}-hermes-${{ env.HERMES_VERSION }} + + - name: Build Hermes (if not cached) + if: steps.hermes-cache.outputs.cache-hit != 'true' + run: | + git clone https://github.com/facebook/hermes.git hermes-src + cd hermes-src + git checkout ${{ env.HERMES_VERSION }} + # Remove .git directory immediately to save ~100MB in cache + rm -rf .git + cd .. + cmake -S hermes-src -B hermes-build -DCMAKE_BUILD_TYPE=Release -G Ninja + cmake --build hermes-build --config Release + + - name: Install npm dependencies + run: npm install + + - name: Configure project (Ubuntu) + if: runner.os == 'Linux' + run: | + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DHERMES_BUILD_DIR=${{ github.workspace }}/hermes-build \ + -G Ninja + + - name: Configure project (macOS) + if: runner.os == 'macOS' + run: | + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DHERMES_BUILD_DIR=${{ github.workspace }}/hermes-build \ + -G Ninja + + - name: Build project + run: cmake --build build + + - name: Upload showcase executable + uses: actions/upload-artifact@v4 + with: + name: showcase-${{ matrix.os }}-${{ matrix.build_type }} + path: build/examples/showcase/showcase${{ runner.os == 'Windows' && '.exe' || '' }} + if-no-files-found: error + + - name: Upload hello executable + uses: actions/upload-artifact@v4 + with: + name: hello-${{ matrix.os }}-${{ matrix.build_type }} + path: build/examples/hello/hello${{ runner.os == 'Windows' && '.exe' || '' }} + if-no-files-found: error + + - name: Upload dynamic-windows executable + uses: actions/upload-artifact@v4 + with: + name: dynamic-windows-${{ matrix.os }}-${{ matrix.build_type }} + path: build/examples/dynamic-windows/dynamic-windows${{ runner.os == 'Windows' && '.exe' || '' }} + if-no-files-found: error From ff9398ee11ccc194d6d64533a8e0f3837f18cd4e Mon Sep 17 00:00:00 2001 From: Tzvetan Mikov Date: Thu, 13 Nov 2025 21:00:34 -0800 Subject: [PATCH 2/5] Add configurable Hermes build type option Add HERMES_BUILD_TYPE CMake option to allow building Hermes in modes other than Release. This is useful for debugging shermes crashes or optimizer issues. Usage: cmake -B build -DHERMES_BUILD_TYPE=Debug cmake -B build -DHERMES_BUILD_TYPE=RelWithDebInfo Defaults to Release to maintain existing behavior and build performance. --- README.md | 9 ++++++++- cmake/HermesExternal.cmake | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3fac0ba..4a23cf5 100644 --- a/README.md +++ b/README.md @@ -1028,7 +1028,14 @@ Hermes is **automatically** cloned and built as part of the CMake configuration ```bash cmake -B build -DHERMES_GIT_TAG=abc123def ``` -- **Always Release mode**: Hermes always builds in Release for optimal performance +- **Build type**: Set via `HERMES_BUILD_TYPE` (default: Release) + ```bash + # Build Hermes in Debug mode (useful for debugging shermes crashes) + cmake -B build -DHERMES_BUILD_TYPE=Debug + + # Or RelWithDebInfo (optimized but with debug symbols) + cmake -B build -DHERMES_BUILD_TYPE=RelWithDebInfo + ``` - **Per-config isolation**: Debug and Release builds get separate Hermes clones ### Building the Project diff --git a/cmake/HermesExternal.cmake b/cmake/HermesExternal.cmake index a27c6ac..af565ae 100644 --- a/cmake/HermesExternal.cmake +++ b/cmake/HermesExternal.cmake @@ -77,14 +77,20 @@ else() CACHE STRING "Hermes git repository URL") set(HERMES_GIT_TAG "80359d48dbf0a108031d69c8a22bad180cfb4df3" CACHE STRING "Hermes git tag/commit/branch to build") + set(HERMES_BUILD_TYPE "Release" + CACHE STRING "Hermes build type (Release, Debug, RelWithDebInfo, MinSizeRel)") # Set Hermes paths - everything in build directory set(HERMES_SRC "${CMAKE_BINARY_DIR}/hermes-src") set(HERMES_BUILD "${CMAKE_BINARY_DIR}/hermes") + message(STATUS "Hermes git URL: ${HERMES_GIT_URL}") + message(STATUS "Hermes git tag: ${HERMES_GIT_TAG}") + message(STATUS "Hermes build type: ${HERMES_BUILD_TYPE}") + # Configure Hermes build options set(HERMES_CMAKE_ARGS - -DCMAKE_BUILD_TYPE=Release + -DCMAKE_BUILD_TYPE=${HERMES_BUILD_TYPE} -DHERMES_ENABLE_TEST_SUITE=OFF -DHERMES_BUILD_APPLE_FRAMEWORK=OFF -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} From f23d4e3ed636eb0ed660b17a09ec3622da980b75 Mon Sep 17 00:00:00 2001 From: Tzvetan Mikov Date: Thu, 13 Nov 2025 21:00:55 -0800 Subject: [PATCH 3/5] Add -Xline-directives flag to shermes compilation Enable line directives in shermes output to improve source mapping in stack traces and error messages. This makes debugging JavaScript code compiled with shermes significantly easier. Applied to all build configurations (not just Debug) as the overhead is negligible and the benefit applies to all scenarios. --- cmake/hermes.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/hermes.cmake b/cmake/hermes.cmake index b7ec800..7e88d1f 100644 --- a/cmake/hermes.cmake +++ b/cmake/hermes.cmake @@ -74,6 +74,7 @@ function(hermes_compile_native) $<$:-g3> --exported-unit=${ARG_UNIT_NAME} -Xes6-block-scoping + -Xline-directives ) if(ARG_FLAGS) list(APPEND COMPILER_FLAGS ${ARG_FLAGS}) From a86e558b6ea712179d955f04a6ec17493b18953a Mon Sep 17 00:00:00 2001 From: Tzvetan Mikov Date: Thu, 13 Nov 2025 21:01:49 -0800 Subject: [PATCH 4/5] Add RadialMenu custom widget example Implement an interactive radial menu widget to demonstrate creating custom ImGui widgets using the draw list API. The widget features: - Circular pie menu with dynamic sector count - Mouse hover detection with visual feedback - Click handling with React callbacks - Path-based drawing for smooth filled sectors - Text rendering with automatic centering - Efficient rendering with proper draw order Implementation: - lib/imgui-unit/renderer.js: renderRadialMenu() function - examples/custom-widget/: New example application - README.md: Documentation on creating custom widgets The example shows how to use ImGui's immediate-mode drawing API (_ImDrawList_*), handle mouse interaction, and integrate with React's component model. Custom widgets use _igDummy() to reserve layout space and are integrated via the switch statement in renderNode(). --- README.md | 57 ++++++++ examples/CMakeLists.txt | 1 + examples/custom-widget/CMakeLists.txt | 7 + examples/custom-widget/app.jsx | 49 +++++++ examples/custom-widget/custom-widget.cpp | 5 + examples/custom-widget/index.jsx | 19 +++ lib/imgui-unit/renderer.js | 164 +++++++++++++++++++++++ media/custom-widget.jpg | Bin 0 -> 22510 bytes 8 files changed, 302 insertions(+) create mode 100644 examples/custom-widget/CMakeLists.txt create mode 100644 examples/custom-widget/app.jsx create mode 100644 examples/custom-widget/custom-widget.cpp create mode 100644 examples/custom-widget/index.jsx create mode 100644 media/custom-widget.jpg diff --git a/README.md b/README.md index 4a23cf5..a21217a 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ _**Note**: This project is an independent experiment and is not affiliated with, - [Table Components](#table-components) - [Drawing Primitives](#drawing-primitives) - [Adding New Components](#adding-new-components) + - [Creating Custom Widgets](#creating-custom-widgets) - [Architecture](#architecture) - [Three-Unit Architecture](#three-unit-architecture) - [Component Overview](#component-overview) @@ -763,6 +764,62 @@ const [enabled, setEnabled] = useState(false); The FFI layer provides access to the entire Dear ImGui API, so you can expose any widget you need! +### Creating Custom Widgets + +While you can compose existing React components together, Dear ImGui's real power lies in creating fully custom widgets using its immediate-mode drawing API. The [`examples/custom-widget/`](examples/custom-widget/) directory demonstrates this with an interactive radial menu. + +![Custom Widget Screenshot](media/custom-widget.jpg) + +**How custom widgets work:** + +1. **Add a render function** in [`lib/imgui-unit/renderer.js`](lib/imgui-unit/renderer.js) that uses ImGui's draw list API +2. **Add a case** to the switch statement in `renderNode()` +3. **Use `_igDummy()`** to reserve layout space for your custom drawing +4. **Handle mouse interaction** using `_igGetMousePos()` and `_igIsMouseClicked_Bool()` + +**Example: RadialMenu** ([`lib/imgui-unit/renderer.js:607-752`](lib/imgui-unit/renderer.js#L607-L752)) + +```javascript +function renderRadialMenu(node: any, vec2: c_ptr): void { + const drawList = _igGetWindowDrawList(); + + // Get cursor position and calculate drawing coordinates + _igGetCursorScreenPos(vec2); + const centerX = +get_ImVec2_x(vec2) + radius; + const centerY = +get_ImVec2_y(vec2) + radius; + + ... + + // Draw filled sectors using path API + _ImDrawList_PathClear(drawList); + _ImDrawList_PathLineTo(drawList, centerPtr); + _ImDrawList_PathArcTo(drawList, centerPtr, radius, angleStart, angleEnd, 32); + _ImDrawList_PathFillConvex(drawList, color); + + ... + + // Reserve space in layout + set_ImVec2_x(vec2, radius * 2); + set_ImVec2_y(vec2, radius * 2); + _igDummy(vec2); +} +``` + +**Key ImGui drawing functions:** + +- `_ImDrawList_AddLine()`, `_ImDrawList_AddCircle()`, `_ImDrawList_AddRect()` - Basic shapes +- `_ImDrawList_PathArcTo()`, `_ImDrawList_PathFillConvex()` - Complex paths +- `_ImDrawList_AddText_Vec2()` - Text at specific positions +- `_igCalcTextSize()` - Measure text for centering +- `_igGetMousePos()`, `_igIsMouseClicked_Bool()` - Mouse interaction + +**See the full implementation:** + +- Code: [`lib/imgui-unit/renderer.js`](lib/imgui-unit/renderer.js) (`renderRadialMenu()`) +- Usage: [`examples/custom-widget/app.jsx`](examples/custom-widget/app.jsx) + +This pattern works for any custom widget - graphs, charts, dials, custom controls, visualizations, etc. The draw list API gives you complete control over rendering while ImGui handles the window system and input. + ## Architecture ### Three-Unit Architecture diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c5e7ac4..98d6d9e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -6,3 +6,4 @@ add_subdirectory(hello) add_subdirectory(dynamic-windows) add_subdirectory(showcase) +add_subdirectory(custom-widget) diff --git a/examples/custom-widget/CMakeLists.txt b/examples/custom-widget/CMakeLists.txt new file mode 100644 index 0000000..d13d43c --- /dev/null +++ b/examples/custom-widget/CMakeLists.txt @@ -0,0 +1,7 @@ +# Custom Widget Example + +add_react_imgui_app( + TARGET custom-widget + ENTRY_POINT index.jsx + SOURCES custom-widget.cpp +) diff --git a/examples/custom-widget/app.jsx b/examples/custom-widget/app.jsx new file mode 100644 index 0000000..a0b5568 --- /dev/null +++ b/examples/custom-widget/app.jsx @@ -0,0 +1,49 @@ +import React, { useState } from "react"; + +const ACTIONS = ["Cut", "Copy", "Paste", "Delete", "Save"]; + +export function App() { + const [menuOpen, setMenuOpen] = useState(false); + const [selectedAction, setSelectedAction] = useState(null); + + const handleItemClick = (index) => { + setSelectedAction(ACTIONS[index]); + setMenuOpen(false); + }; + + return ( + + This example demonstrates creating custom ImGui widgets. + The RadialMenu is a custom widget built using ImGui's draw list API. + + + Click the button to open the radial menu: + + + {menuOpen && ( + <> + + + + + )} + + {selectedAction && ( + <> + Selected action: {selectedAction} + + + )} + + Hover over menu sectors to highlight them. + Click a sector to select an action. + + ); +} diff --git a/examples/custom-widget/custom-widget.cpp b/examples/custom-widget/custom-widget.cpp new file mode 100644 index 0000000..b1b026e --- /dev/null +++ b/examples/custom-widget/custom-widget.cpp @@ -0,0 +1,5 @@ +// Custom Widget Example - Demonstrates custom ImGui widget creation +// This example shows how to create custom widgets using ImGui's draw list API + +#define PROVIDE_IMGUI_MAIN +#include "imgui-runtime.h" diff --git a/examples/custom-widget/index.jsx b/examples/custom-widget/index.jsx new file mode 100644 index 0000000..99d518d --- /dev/null +++ b/examples/custom-widget/index.jsx @@ -0,0 +1,19 @@ +import React from "react"; +import { createRoot, render } from "react-imgui-reconciler/reconciler.js"; +import { App } from "./app.jsx"; + +// Create React root with fiber root and container +const root = createRoot(); + +// Expose to typed unit via global +globalThis.reactApp = { + rootChildren: [], + + // Render the app + render() { + render(React.createElement(App), root); + } +}; + +// Initial render +globalThis.reactApp.render(); diff --git a/lib/imgui-unit/renderer.js b/lib/imgui-unit/renderer.js index 9cd940e..32721c2 100644 --- a/lib/imgui-unit/renderer.js +++ b/lib/imgui-unit/renderer.js @@ -600,6 +600,166 @@ function renderCircle(node: any, vec2: c_ptr): void { } } +/** + * Renders a radial menu component - a circular menu with selectable sectors. + * This demonstrates creating custom interactive widgets using ImGui's draw list API. + */ +function renderRadialMenu(node: any, vec2: c_ptr): void { + const props = node.props; + const drawList = _igGetWindowDrawList(); + + // Get menu properties + const menuRadius = validateNumber((props && props.radius !== undefined) ? props.radius : 80, 80, "radialmenu radius"); + const innerRadius = menuRadius * 0.3; // Inner circle is 30% of outer radius + + // Get items array - return early if not provided + if (!props || !props.items || !Array.isArray(props.items)) { + return; + } + const items: any = props.items; + const itemCount = items.length; + if (itemCount === 0) return; + + // Get window cursor position - this is where we'll draw + _igGetCursorScreenPos(vec2); + const winX = +get_ImVec2_x(vec2); + const winY = +get_ImVec2_y(vec2); + + // Menu center is offset from top-left by radius (so full circle is visible) + const centerX = winX + menuRadius; + const centerY = winY + menuRadius; + + // Get mouse position for hover detection + _igGetMousePos(vec2); + const mouseX = +get_ImVec2_x(vec2); + const mouseY = +get_ImVec2_y(vec2); + + // Calculate mouse position relative to menu center + const dx = mouseX - centerX; + const dy = mouseY - centerY; + const mouseDist = Math.sqrt(dx * dx + dy * dy); + const mouseAngle = Math.atan2(dy, dx); + + // Calculate which sector the mouse is hovering over (-1 if none) + let hoveredSector = -1; + if (mouseDist >= innerRadius && mouseDist <= menuRadius) { + // Adjust mouse angle to match sector drawing (which starts at -PI/2, top of circle) + // atan2 returns 0 at right (east), we need 0 at top (north) + let adjustedAngle = mouseAngle + 1.57079632679; // Add PI/2 to shift origin to top + + // Normalize angle to 0-2π range + if (adjustedAngle < 0) adjustedAngle += 6.28318530718; // 2*PI + + // Calculate sector index (0 starts at top, goes clockwise) + const anglePerSector = 6.28318530718 / itemCount; // 2*PI / itemCount + hoveredSector = Math.floor(adjustedAngle / anglePerSector); + } + + // Check for clicks + const wasClicked = _igIsMouseClicked_Bool(_ImGuiMouseButton_Left, 0); + + // Color palette + const baseColor = 0xFF444444; // Dark gray + const hoverColor = 0xFF666666; // Lighter gray + const borderColor = 0xFF888888; // Light gray border + const textColor = 0xFFFFFFFF; // White text + + // Allocate center point buffer + const centerPtr = allocTmp(_sizeof_ImVec2); + set_ImVec2_x(centerPtr, centerX); + set_ImVec2_y(centerPtr, centerY); + + const anglePerSector = 6.28318530718 / itemCount; // 2*PI / itemCount + + // First pass: Draw all filled sectors and outlines + for (let i = 0; i < itemCount; i++) { + const angleStart = i * anglePerSector - 1.57079632679; // Start at top (-PI/2) + const angleEnd = (i + 1) * anglePerSector - 1.57079632679; + + // Choose color based on hover state + const sectorColor = (i === hoveredSector) ? hoverColor : baseColor; + + // Draw filled sector using path API + _ImDrawList_PathClear(drawList); + _ImDrawList_PathLineTo(drawList, centerPtr); // Start at center + _ImDrawList_PathArcTo(drawList, centerPtr, menuRadius, angleStart, angleEnd, 32); + _ImDrawList_PathLineTo(drawList, centerPtr); // Back to center + _ImDrawList_PathFillConvex(drawList, sectorColor); + + // Draw sector outline + _ImDrawList_PathClear(drawList); + _ImDrawList_PathArcTo(drawList, centerPtr, menuRadius, angleStart, angleEnd, 32); + _ImDrawList_PathStroke(drawList, borderColor, 0, 1.0); + } + + // Second pass: Draw radial lines on top + for (let i = 0; i < itemCount; i++) { + const angleEnd = (i + 1) * anglePerSector - 1.57079632679; + const lineAngle = angleEnd; + const lineStartX = centerX + Math.cos(lineAngle) * innerRadius; + const lineStartY = centerY + Math.sin(lineAngle) * innerRadius; + const lineEndX = centerX + Math.cos(lineAngle) * menuRadius; + const lineEndY = centerY + Math.sin(lineAngle) * menuRadius; + + set_ImVec2_x(vec2, lineStartX); + set_ImVec2_y(vec2, lineStartY); + const lineEnd = allocTmp(_sizeof_ImVec2); + set_ImVec2_x(lineEnd, lineEndX); + set_ImVec2_y(lineEnd, lineEndY); + _ImDrawList_AddLine(drawList, vec2, lineEnd, borderColor, 1.0); + } + + // Third pass: Draw text labels and handle clicks + for (let i = 0; i < itemCount; i++) { + const angleStart = i * anglePerSector - 1.57079632679; + const labelAngle = angleStart + anglePerSector / 2.0; + const labelRadius = innerRadius + (menuRadius - innerRadius) * 0.6; + const labelX = centerX + Math.cos(labelAngle) * labelRadius; + const labelY = centerY + Math.sin(labelAngle) * labelRadius; + + // Calculate text size for centering + const labelText = String(items[i]); + const textSizePtr = allocTmp(_sizeof_ImVec2); + _igCalcTextSize(textSizePtr, tmpUtf8(labelText), c_null, 0, -1.0); + const textWidth = +get_ImVec2_x(textSizePtr); + const textHeight = +get_ImVec2_y(textSizePtr); + + // Draw centered text + set_ImVec2_x(vec2, labelX - textWidth / 2.0); + set_ImVec2_y(vec2, labelY - textHeight / 2.0); + _ImDrawList_AddText_Vec2(drawList, vec2, textColor, tmpUtf8(labelText), c_null); + + // Handle click on this sector + if (wasClicked && i === hoveredSector) { + safeInvokeCallback(node.props.onItemClick, i); + } + } + + // Draw inner circle (center) + const centerCircleColor = 0xFF333333; + _ImDrawList_AddCircleFilled(drawList, centerPtr, innerRadius, centerCircleColor, 32); + _ImDrawList_AddCircle(drawList, centerPtr, innerRadius, borderColor, 32, 1.0); + + // Draw center text if provided + const centerText = (props && props.centerText) ? String(props.centerText) : ""; + if (centerText !== "") { + const centerTextSizePtr = allocTmp(_sizeof_ImVec2); + _igCalcTextSize(centerTextSizePtr, tmpUtf8(centerText), c_null, 0, -1.0); + const centerTextWidth = +get_ImVec2_x(centerTextSizePtr); + const centerTextHeight = +get_ImVec2_y(centerTextSizePtr); + + set_ImVec2_x(vec2, centerX - centerTextWidth / 2.0); + set_ImVec2_y(vec2, centerY - centerTextHeight / 2.0); + _ImDrawList_AddText_Vec2(drawList, vec2, textColor, tmpUtf8(centerText), c_null); + } + + // Advance cursor to reserve space + const menuDiameter = menuRadius * 2; + set_ImVec2_x(vec2, menuDiameter); + set_ImVec2_y(vec2, menuDiameter); + _igDummy(vec2); +} + // Tree traversal and rendering function renderNode(node: any): void { if (!node) return; @@ -691,6 +851,10 @@ function renderNode(node: any): void { renderCircle(node, vec2); break; + case "radialmenu": + renderRadialMenu(node, vec2); + break; + default: // Unknown type - just render children if (node.children) { diff --git a/media/custom-widget.jpg b/media/custom-widget.jpg new file mode 100644 index 0000000000000000000000000000000000000000..19573ce19ec1123695bbb032c21be4aea5b10e44 GIT binary patch literal 22510 zcmb@tc|4Tg`#*k8<1r?VlyF@y{X#eeghK6R=X@-O#dvcKFR8pY6mu=r!jKY75y zM?up}%fJ9k&76Z=UHyVR{6hfX5Dmu|XlXfIGBwsUxS#_-KlYGa zs&`J%%GyQ{whn5672p8Q0%VTP!2z0=E?xM=^-p?){|&gQBRv7YgwzqQf9d~Mh{?q@ z*csIFB`~*>OR%#qh#NrVv^fU^fo-M?0O`WvApuADHi%h+zy^Z&$q{z{joXf}({DWb zE04LU7MSM~*qL;WjzR7Kz<>eif?>|?ARjm*h)?;sc=~}@1H|gSzWy#Cz6N4(Fb`NK z{D{Aj(|_=B{}1fw==2XyM@RR6@NZebl3>N!p1}b=j*-6}{{Q^(^$7*_^(%;hzjPi! z`levY3hM2Xx4+>LrUNnF&*`Euh{4fBM(znp_Xo3kgz8@gF_@oB+&Nea)Fr?UV%1k$63?RN3 z>}__`9KKP$$sw5D#;G5QDmcesXsF)qYSGD8|)K^N)U@xvs%hM{PUO;gpN3who9v*~zI~ z{VpHL3)0Cs147On)diL%mk99r^_?8i&$;^O9np`0*gQDQ>~GnykRbCTI@nI~q=1l1 zM>+)CNuKZJc>WTI!S;|p11W13gA0GkhW@Ql z5dOD3r4FSwr9P!DYQ@(bXv1{h_@mB`QjmE+(16rU-^D9{v>6iXB{4xaHr z|4na3z!E6YZ@s#LI{y#-kRJzm%9EcXR|C0&oXOeAxdAEiGhjWM>WXZ`=a z@dRAJb{hWE8-H2=O@`J(2cg~2A!r-484!eaLSI1NK--V-pZaxw^{)FrwVD2Ek2lya z|9#8|f+Oa4JCDXk@Tsa(>!*mnxrDezgn(ll(DDz64DxjM2ocl-R}5D{13zc!lY+9R zWaR6;mdz%=Q{zQP7$0ETK=VpD**r#Edab~ zaSja%`^}I1D4_sn4mkMraRK~*Fdz=fBMT@1Du4!{1Dpqp0dv3_um{Jh2RP1yfN%f_ z!~sb_8gLW11LOlmKnd^!s0N+`%|IK_4GaLoz$h>Y%z>k471#tY01hBPAP^XY9>N0Q zf(SrFA(9YTh!R8{q64`AF@;z|u0q@)evnWI5^^1q2FZcsLmomZAa#%yNH=5%G6tD} zEJM~I-yr*BP%=6)Rx&=a<76^qN@SX3hGgbsc4Y2kfn-r+31pdMcgY@+Jtb=*>n0l} znwVMXCi5l)dr zkxNlV(MZt`eoLzqI7&)Nc1kfyMetj+ru3qWrc9@NKv_%KML9;fLirO0gK@zmVQMgA zm?JC%mIS*8dkX7>jl))9cq&>d0V-K4T`C(YKdN}DT&hZ{4ytjgH7Wu%Bef{CGPNFMc@(`(S%(1*}x(pS*;(l5~C8CV#k z84MU)7*Gra3{4E<3}4~Ya8bA>+#VhU&xO~+N8nqG)Qra&wHO^4V;Kt=TNtMqe=@N! zon|s-@?}bAs$?2s`oaujKF+Me?82PDT*BPTyut!y5oXb1ab~&B@|dNcWtEkZRgBeu z)r&QawVL%ED~64k?F^d*TLfDHTRYnlJCyx6y8*iodlvh1_9^y#4n7V|4p)v8jv9_} zj$KY3P7O|1&Q#7?&PmRFEZRa zJhQxHyb`>oywSWRyl;4a^6~TO@dfbRLc&ELVlF2Ew7F5oF}TcA^5 zLy%2SQ_x57u3(=aMu=BPUno@Qq0l=a!ZER9myg9Ct35U^Oe3r$>?V9$xJMWxA|PTU zf)uF|nH8lLRTA|O%@Z9I-90XL-17L1Ol}eQ_s$#0Hs*hDy)Q+h+sy$R&QWsRes$Q(Vgb+eFA|4^o z8p0Yb8f6-5nkO{9G@ojIJ$LF{(7DEQyIRUxFPP;EW!RPA><%sQ8K@^uz;kLkMW zR_S8&|(> z#V?0m?y-PdT(PLI*tOKQ%&}aulCg@h8n))P_OyOs1GBNPDYN-$t7Ch|cJ+$15z^-wETa<(%id>7wCs+hyHV z%{9k$%}vcM$L))|n)@yHbq|C`uE&GVNh8xB-k$aMF=an(SH*v9-0`s9Ht&t5QY!8 z2(OP|itvkg6Dbjy61f(o8&w)j9_<|6gA_u>A(vw`V;;srVx3}pP{ODL)ao_8Yfs{6 z;=JSD#-EDMiT`ok@_K6mU&6J7l|;S7sw9S_prq*=DmRLf$&)>k-=)Z<+)E*)x}?5N zJC&B3Mof23f1PnU<8H=brd#H_EQPGXo0K>GZcb$*vY+HIg^wQ z9PSL|p2;oBqsa@+Te^GU?u&aO_io-J=6mE%7Mv@nz0Z3;<^Jvimj~m88ih4Qd_`$R z`^BEcA0FyGYtAlh)hk)<)U3DOkV zglYC@UV35uVyxwSOHZpxYvW7lm*s6jZ3XRY?O7c(9SI$Wol%`Xy8^nly4|~1dK`M@ zdaZij_nGv)>o@FwHK04tJE%F>`AY3o+mP~5%WI|A&BF@AO>gAiG`^L8+xSl5UDJr- z$cxdlqpf4AV;$p&@$L!jiGlY9?}sNZPL54op87C-WqRp@%ZD#BJ~P#?&45k9R?;@p_WiHoUmJEbcHUsDFss-g?BTcc?_A%@f1LTz^YhZr1)MLAu$zkK z!avzl+8f-r++QPv6Jf-=2PY0%Nrt44hrWl00ggeAzxD!0);*aUXrF&82LO6&0ALvf z_Ybze?ZaO=e%bzi5NJ;QLci1hz`t$CU+)wEpa$I2n{0u*n`r=e_80(;gZ$xF0N@fm z0GNZ8w4B^O@ejM-cIVLpC1_U^I~+LqI{kU|Uy|Q8DMG(#Xl3IV$sC(!)6* z0W|ZE0vY%RB_kt)l0(4&F8xP=5(ci`U;tO?UxDgZpgs!VqWUL5j!HqH6yP5%6(!YQ z^8bB$I1l*1Ln$c8!QC4q27CvQGf^-Loz|peF>!<)3uKjz%YH~De6FsW&2$VSBIgtoPff$l z!O6ufdR$EWgv1$n1x2N^%39hwx_bHs=grJ7TUdhIL}wRQH+K(Dui%i-u<(e;sOt%d zNjH*HQgd$IzLT4G_g?;^lEmL|=HS~IX;{D{*^oN<* zW%SCY)z52R);F-vi}bw&I^& ze8UENU$}c#yZG)-qkeC%_?39ZZegZ)GGbRyFWb-NM3ftR#%#IIWZs)L68+Zh6z$_W zqtX|ghz&Q5NXJ6}dH)jZQ||SpPR@ujO?Xm6+JSb_-uH(5mGXli-5<4&RiOkHA`-hI z{Z@TZ&tionW@=P+s(VmUzC#ahxj|o>6|zWQ+>8%iZs+J_DyFF}wU)Ynm)AAjhZ3!P zKggga`ZBgQJ#=5>jn-m`3E|fb2$Y|H?zGMpRn!}6BM`1Gl1ZmPx7)tWW zqWuaf+G_g{7`3u|i1)@J4sH>(mi?I`Qh5y6kO*IjzM?OW{aA)p<8}$ojZT*B#FK|W zjxiQN>W&I0-H`{7ortY&f~Z|an*4TNn}Tg;!IUELwBlANG7IO=htK5tkW+_poqOuN z{i0U$XASY^<6<~;8nyJG7KuX7;LB!zX3cXqPPXUZHcS*k7-%2lYvF8b+(@hm7ud7tKU|>mVB$pGQhg0h5#k!-q-jvIs0cz>ZM2d_5-of z_#4ZIKqm3{9MbsVhE15t{D+UW@OM_@Eyt*qrg!=lnHH~T!ut4F46s-1rfnMuVSy7V zJZ!7{q6ZH;T^3VJUS_C&@0ra$1k7=@6lMAMA>Z|w+0`J2Ku5a=6dRUGm_WmcEZ@m{y!_ogank6En{QQbuH>9lk4Rhxqu3a*Ug7+*YpcvKVA~KLN zn6KTn<)4NH$@~4Td&pk6BzMxwI-Iebi`Q~fpx1bYuvEUaS624$sSYe%)g~76!pi+w z?_{)YedU9&?5t5Nv0+%!gn!yw8?tMS%CU>e=NF`rqB|1c$zNjD7w2gvM2Y z>Ymnpj!N~y!_HEnz{-ohk3%{J?_^AP0O3#dx;WfXhFU|+1_ zdqe}#BxE-ylXdvJFK>Vnj_u7Q%CGfkyI}iP@Aiw!_X_alTbe&~da}AK*jO(?bpAy9 zT}(a60MT~{5Gd9c_su7l?(Cpw5ua`mjSqom_74t$QX>t1ny7Kp!iL*`i4H4ZnyGAR zqQTwj)>7rMOB-5#Mb+m4rGZ0#6w{VYsJg^Ud?2=RK#gOiFjMB9u5f!(uAv-<@t(UE z3u@kEX#1;o+@ovi7x04ws)+iYSxNl&Wzlc=qTUU4w-dPN(w?t9wthL5uPf9bL$nSs z`}O^#0y$eRg>Q5E2^9t<9(S1r=dV%`t9GHig(6c#(aPiX%fm?xEf-?Hos_)F73&K4U!SZ!~l**!s2Dff-u)_gB}+|W1cYPRT`L@upkH`|SUesn(H*(wRe=3A@U ztqkVrV-oNBH_WrniBvUHT(x%o_BpV{O3+VFO$kw2D2r_@#pU@fpe3> z(JEAhGJG#->(xpmPx8=8Dp2KF9pSJ%l6VL#+Y$Mj*loNV?YD^2a?4?#jJ~~MtdXsr zO!B@K9?3f-bn;D*!KWP0hJOda|NCG#8W6wYL@Dbf*rQo|vk?}4!vzEDt8%RL$O@8G@MiApoO`Rlqz{u1$~^L%6hMge6-iC~yLu6Yi$Mji79k24qFT7EyJ++@yq{wuoYcWbe&=2xVt|1j5G7Tu5 zxw8)?SdxZ@m);O9@tU8#3OPH%JWUwyRl9^=7DA`+INjq-klu5>k{Rq!R2&bn*Id`2 z{VO+^!Vbs(?Mlf_{}}|%uLHl1&YS!h&R)npCTcCuQ^sZLbb1&Un5lhBr%g!W64Uo6 z%Pb-%UJKos(V_kv!;#=LuQtKx;-4!XU-CkFZB+yLyd;oImBAb5Z+D|t0FG4g#Dp&w z6m1!ao{Rk8>V3Vpif_+#t|gBrQ*Tv+0UMt3Be;AP5ARagERXOuxSTq^aEj#(6It4s zhw6HFZb24-CN;?s@kiHaR?6`inA!30#+O)y47xhzES?Sajn}>LH1{CnA;OI@P>cqW z(?5fs#bH)c^w#p&r0-y=DZF8?x@7G@T?xcgYBK;nL3X|Vs&2ck-`Ek4gQON&{5-)nx()_ORYVO~|ETBJYame9Me#R6|b z*?#Wy9s(4xBSj5}ZM&zQ;THMe%DHnR>M;k$6uS-ogG!`E`*>+n(mVM^NmiqnK)${r z&lif!f>yH6?w`o4CCwE*Pr64qg_KCcrQi*!y*f_gg9GIUvKUIxe3XP9oBM@=4bNNO zyVlz&kXY}|(W-=owqF?-m(!;FvIpSbr4l-EB?n1J69R%X73Huswp5j2IAFoujAOC& z4%@#{#ap-GCeWmMPDbH`T&~-i`YJ6ZxR$qQ&(%tcRXO6tnwt3K+F=1&9T=l4i>qro6&7onD z1L0|$x`@#qNskpfNIC?Z=L)=O7b)yD{a4^D8x}V=Oe5+|TPH&1hP}t!gL3rBV)7S) zy{X-%jOcJs?qC%ij<>A98(uV$ze?Ai~-t7p#^WY`1?dwdTqB9q|)^clS>(-M0Oz!|F@*$AQUeqv{S=+(u z5l9T05XrKfP(=iyCG%bw)2$PvNN+vLE@jf@;AdY3PA10M6N523T_e;;gFKCc`$*R( z5r;reinF(@cT{dl;PU#yH;dcG{y51->-e4uCgv5NLPy$NdLUAi)kK4DH?GZ##93&h zVj3@baD10K)HSHkhZ*wVsk6ul7qK8_hAAMGh&iY|1l-i*mJU7&Fp_xcin^m-KAM%g zX+N2on7LrFr}es<>pS=MV5dWa_e)Is*Nms@)1CClK*C#?UCOrm=3V4ZCN4|=x8KSF zD#2|n5KCj?pg|g2gb+0`JZSpC8$Zbx%Rk-3tk41Si&wN}6)0cj$iK5H5hZi_b>RwXkr;ziP=JeK?!- z(wWV-tNc<&qc)(#YjFsqG;Y>1dxvwkj~xP5+jY?*k)q~N6fl4R=4kb64S~E|j?N;p z#upg3;U9;FV*2Es=GS3<1PoUlq@2y$IuXE4*)od=dVZbDhmx~)GAphKSUb2U@6Q-< z19uiQ@o9*n*ZVo>b|nSUe$3H(Z47XMj-tsDWuH3QFIV#pM4cg~%cYn16yo;SEOfTf z*B)5{s0I%xZl|lL;l@@eqOUfC8?hx+8G*vtPPaKcU60C(L%O2bo?$-U0e89dA8gl8 zc`IAaqQ-M(eb28-OJ8kF~&?*ol<(CDk| zmTu6xv&HZaGBYsTCT4K!fFM2-k@?M+VS1pm{zr3bcyA~Da5P&zK4%eqvEmS5nGtDZ zO8Mx?i1r{2ysqH4SqUdct{j3ViTIsj(yLA;q+Z!vL>7s0c!!xNKS_ixFS2|zpl>fr zsah;R)OlHcA11tz@|}G@tlD!{Awc`0tmIO_*UC%0CpJ_9^`E}dI?>H{T0@}DpdtumC+hi7uamoeOcZgmK2?qhh?-=o<-BbQpHVYwhN-K zFjs$b>g2P%m2Ent`W-kP<`XjXR_phGj9%;+*%JX}I0Uvh%2DfPmhu17^ocmJX>dMj z{MVKRIGtVCC-Az{QvF12w?F=be_ypRu68>1WCZOJDl>HN2J!g5ai38vAJ4@X@3*%a z2+;;eC1m-F7j^76Z@a>KT9j?;W8*5v(|v8piR%j2llKn;PiPRd4s8ULk)PV7sZ zEhevzDf$qg?Oe7&s^kr{zMW5qWtbpdd_+QT;M8>Iog|l^(;2#liv-)r>653u&i}E) zj<;MA+gb#bzJx!(v<~8~;f=A9;KrJ%@esI?$E}{VgksUDa%iwQ-pe)zB25ocRow~n5xs*8 zoOs6?ZxoZ1x{%E#N$I`w7*Tb~CiaeNnS9Y~<}Lx5BRR}^>{vCO-(0t8BQ|efG08@t zdolTW1gy711ykNvV72AzulsVTTH9+@t|0f`1==7cNTGn3oL=)MX1#;2s?X25Bz#Rt zExaFSAd+)_Zq;e<{dWzzN2%h#V?n0th|Gg#bEBs_K4b^?Q=cf4LkBKzCY%1JYl)SuWCSVJ@w%EL@3YOWBY1)#y9)V z=t&HBb8&8GPJS=i-o`u{G0z>uWnwZ~^}K6Nkft*QnWSpMHNAb}f?)5G2TzwRsQZDv zb+6&iRvdNs{`XuqjUnX)R&O@hs8w=AS0!Hs;CmYzMK|BZuGS{{b}%5tMq{aF`&q-o zM!VIUS9z1(FByw8>5~)VB84KLU>?*`kpo>r5gYMRJ3bA4x9xzXXOyi~vw5{}TVU($ zcNaPX0Ricl(++OlHd7DLMm8&b+;#Jz5eq(j;X#0!ff{JAO8+lb=0BEa`_8gEF$a}$ z2;gWZI)7ADbxSUg*il1b_@YDLYP-rIa6bQ%*1w^*x#3GGNj&qFQo zCg|jGI~ALZGVRzRzr?Uv?4X`tW`tmX_q)pZ#E-2pKe!V+nQdR?l-KKO!QWqkt)Md7 z*s739*xd3ZzR>gYUv}Z1C)Vi3kdG^bf@GDKVkB?Zj%VS;IeK5z)P%sZY~NCAv!0{e&vX6|2xYC?hZxuFEazRT?GVTB z8sbCC;@ze`MQaZ_jh*F#kakLe*mfif#uIN%koJtS)g7ve$U(3F6vv>#a>MAmyEmg(G3)YS8PfhhywJSW(G!b-^n(x#$c?~g&{b_aM z-?iKQ3T?XCa&T)ZcZ2t70L*#GxgB+?jl77wuKkR>WTuX-FlMe*{yqsv_Tm@s*Ych4 z%|h;it%YL^xjn{T-@SzU4PuJvqQp7X{x?U##Mloz=RU>Z(umk8!?`lTv!0jt_pe%( zeT-n~j-^jh$x4YjXl$r{FvUJ0*sg(S=dA?ZY4X2^e1aH!kz7<~mxL#Pa~u8;xcl3f zwhxWOrY&VAlk^hL@MN49{j%S7s=nyL-bD%{M}oEWx&p`S@w_E}c)x3fZnj+K-8<6dE zrfsW`-{#z3v+h@_%%;de?McaPDRddalcEM5p3h27HWbB^)H~)Bsy$}qtFBpmKCltj z7bbKG_`k9EhSyt$Cq5iWYaEh~d>>zOE|=?}NSL};#oQ=w*jpenJOwGk(GQ=Q8;B(< zRLtKwKk+rxPWp_gjp4K=nRqwnQudJWY$yHB_S@2f{(GFKhDEfaI6qVf#IYTSMDU<@ zt~WSvdNy&(BsF|EX=84fRv|PODq>j3TeU5GEk@4a`eH*~oHU6C=`}@^i7Mdxq*1IOn_mM?#&vVfp#s zpuPzYvV*d!%JsFdqENM#L!eBy4Yg0rwy`fZ2xnh^c5vM^b9{dWA1L3d`Z!vDPdICR z-Cj-yh+cnBqKarkmCueWZ@Hxt9tR(sVoPWQfi^%KwP9wgq_ zwSQp=dwnlPvfE2vB-%6gGo56)&@)+asNv<&#u$b#GaF&stZDCKmiKCOTp9IjaCVX_ zwOQ3Cld`I>Jg_tt__leLTAoQkF^(j2RsKNPm`)CUui;5Xx7f?rA1h+( z8vIb5=+fzK=O?yJB};FejoacPzLmFXzf`}Q!n)RpGPe0n@7~>U5`(j~neeMF*%rB} zEX4oto`!HHJrHbe@&mM0zqU~?dGBz0>5BLt8~?~9?e2IdQ>5EDJwBjCi5DApWg+pA zm%Z?0@#v4_H_eLV$)jWjv$XtxSg~4_`$}G43_^39XDRR6^C*jIalm6YG zW_4DzuQWJ+egO`FWE4+G7LBa{8IfyNYUTAuuZxd|Q~>if+omCgElKgAx$zFOj`xjx z7sFcK`-#5D&NMu~wf5b~$OOtu^LwJLV&4r9%c1u9oWsG#EhgUk#XKHCQ>z2S7-Lpe z-dl>R@?rgCw)v!;*xb1clQRv-hrhq-Qdpo=H)$>nA*o`hF(thfo*craV>{*so)O8; z*3PQXx)j%UaQb-374CiSCA~g^H&rR#2>p#mYpQh5po@L?Vg-%SXi!R_)%H(TNP$ks zf(&MUI#GVFA-XsK5lJn~vlN7E`l9|V9XnVWdQElKO)$n;UU|csW-TkXSd^B_N z*&)EY^TS^jku+=9nK(9p7=2JAn9eUsC;6&zhjG(h?4=?}=!Igiz)I*9(zU2jSRJZI z5#!wbO%aEyMCu6+?yEgFaxy;z+QOlSz|`&&SN!xrwo6DY;Hj`5Q?bI4Js2s@Impb& z5VP6iQOt150*gv%Y)PL@{ILZm)?3>-3uw`H_-qRi^$4f&W>%7I5%wED zP%jTk!f9f;is7&JL8l7ha2lKva56a4COa{lND?NanPcH(XJR|@mS0WhD^y~va%Lmp zGA2mi8EHtJw~?2+kqN9gW~LM{{0~pk(b7m!pfcR99vEEy8l&INuOQxw<*YRtBgsXq zUASzxcWf$K$?B)8_lGV%%IUzt#z-Sp$WCv6Crs7uIMTM{qm|t$w4K_crY46f?mVJ2d+pr1SJ1^M7zL0{7zUKN?6&60wi7--mmn$m8^Lz0jKL05Oa>R{O+GsyOeR)DXF zt~FZ|n=nl6bk?c{*&_c8P}F?HO_f`mJZH!jGC)ON@qSAd@s-KRD27rPd2HA=&8?R@pign%(77WHp|k@ z4lcZLIg$snmii-$$*Wmi<$b8{xd}_mkNpcbJ@;ep`SH%$Ydv9Qx?&JasKyA_>$I&} zI=euJJt~Tu3s;F%E}lGmtS(D2;)S#5N#e^VvkC(Ny}Xym&$#B@9WwinNwsg0h10ES z4xe!yB-|iMBzSy#ktt|ha19m(5S~w=SJ1mXF^YW(n1u7uafM74hBsQUe1qD#1FlxE76WtM!2Q&c z3Z(C3pVLS$x8H-(UV)Ff3-5J;N1i74BJ$2-)!RMAq5f7_nPlQQl3VAKD~pO(K3@1% zM2yJq0~l`qw)+2C{}c!`@sd|jT~Suai_eBUBk*Lzc2jXd9Z5&FwV+C~f=GugyQ!=F zwyX)UdAq+8+_teW++)2Qt+40{Ekb9&KRp@W?JUStyp(4&k$?FyPoSnYe>avXLO@8jv=g%~%Ji7O}*?=#9G2%n2%LgrXw(p4K z&a0@IVRR>vtz=*}E3QqxY%X>ruaN6~$FT?~iRpIzA&}r~m)2vU%#=SRlHw9?q#V62 z{irilUQ2qHX0z?q-X`et6vcl+dDJG`Gvf&lKNvr_N$EUX=vBK6E#T)9v z>$ut4UKI=QMcTId{Ns5kT&?^wxgMIYrz7L72q%-vVVt$O>c?Vd9O^nUFmnAUicZI( zIPQ_27S-@F#Emx17<3k+0GmiYPyaC@Jac4Ht9pxQz)@((S;|W?6;b?IUqc%BYGx9xOL}GV>+3IiSz)F;|X7cCo?Z zkNp=)0?0b356WT%Ny7+cr0rvG#B=vvJJfPmhPUe(?#u_Z_#_9Ww~-;^k~*rvh9?JD z^?QXftSH4q0;so;p#(FlhzT4!-kZSL|L}_K6`~q8FS*TXzt@OIGb|s|LhF|eUl7*9 z#kCZy`nHAcX{g^i$UxF!P|Isuo>)r~4P<2p>VaSht-aA9J*vu49^pK|9=O5Jo4?ps zbk);utw};eF_$p!TWWpTSuen(*{!bAXORIZjt9MI@Vv-&U^Rm&FO(hr-nP)25}|_wIl%|Gv0dz zKC^;L!AOBMCaNoTHcrbM!OnLoD?FZkmoGInfR@q}NkL~)T$kd$|MHH?H40lrT+6{z zb$+5RUf7)=K^iWk?8Mt)3SZ-P`k*?~p^8|BvIUX$x|RvoHqvM=+Ks(zJk-Pk=zHQa z|J~^t!s!t3mq|xGci5^XAZKFxNR$WJ>M$>E`u@)NM==(J9xR_Ws^--Tm)I-0rt4-Ko37;}u41VR z9_Wl8rfx4t(Bp*#@@q+?jigtnMKse|UkkRrwh z9G9MTc$^eWwU((aP0-IC^gQKpC$;VYocR1L5L<$@z`_%d@D%}C4|Nti1wnaeX_1dh zDkLkli1tjIw)Ar9XM^nq!o<`DlYi5)#;owo^c*g|<=7yD+DT*b*m(pb)Hu;*t@{x8 zu(Irs3@(RJ;i)L5ERKj=fA$oq$FiK@l#{S4hjM@8cWT0riqeJWiol&)-=2L7@__u$ z%NnVGWXBj;b8i-_aI^YYS=SAUTBaJXpRq&EHXSozv=Ib17JkwwJR?oH*ZxAf2DjE_}G%IcoG8K}V(VmWyTG{Lrtf(b$qJH6_R;DWHG zhtDX-clPAFr#DmwpL?D=@H$4(B4=3&Co;`JH)D%Br4&$Vr(trv=r(9pRC;mXqAZHk zE1tX(YHrNBm1;5d4_aB zMP!ld3yY>)ml?%d$0yyaE&bo6bP4jK{%ka1)hIXcq;<*u;WCepLGbcm%(-!al}H&8 zss)>ChK$8)&^QN%>1lp)Z0K~ns*x|6$Mw55d2ULQNw>u%t{@0BOgD;qIaUREGOIhP zEmjn>%o2gJpfu#N1bnkxd{kKODTbJ&0vPV2!^Xey+_{Xw;?i0t{AtYV6|_vs^()u% zFWy(5d~!Z8u{pW+-lFiT&l-&XHsos|?RXhJ8pHkh^QMnttaMmIu5?Z$`*Uh1H&Y0W zri!uFY}-IP6s?zP)<+UN1kOQDZaEpMgc#qUv0Y{B;&sjAQX=2CEc)eUg6Qz~badcB zbA_CE(9-FdbpwmEJa|B2^|n8Qc{8o35V#Q)x^Rn?KG0PtDWYe7gMIw-S+<*`Eor^i z`o-{@9+hF>{9QR)jk^`d_MXn_8ch)Sna;3<5w$f6RVC5~=TlD-+;mx8GSg9%00%@c@rSrllY-Rk34wVXk zkO=*Aj=cFphD1GvHgCXZqY*d0q24T)c9)8O9enFa#fu6F>yM4HtS5~KFx^@f7yu7= z3<{DP44c|eyQb8Zlc@F$ap-<39)ha$aKN0eokhnS`|N#o1uM|=1UVeZeS)kxIo%H` zL{4dHzVt)sY3GMUhv`_c*mvOM?%rLLGR=a&-rd2Bzq}%lY7qFN7Lz={B@Za7R$41oOIir$qw6;e`IBcmNShJPY<{7d{db-WAI^CWJq| zWSlLb$FwS5E5R&}PV0}Yz92v@Pm6g!48gon|Db!!opv>y z25*LTPY(f&8J5t_WP3hL*^MP{KBvzo(3ssMso35UPqfmpYo!lfh-l|-J$MT{ATCkxIs{Hz!*1RrI9*k!DJ)_D~?ckYQjb#Rj?PcZdzU_eS@lU4@zGuxeJ z-P7CL72_X{>LvY@%s6ZF+R)H?fw0}JI+O4+_MORPS>Baz-*L|l9wB82+G0IAn2x+B zo+wOB2Rq)mOR;m741z1zrrlN#?eEM!+4*HWa&tAZ@JbusSS{Dr4gsR~L0ZSL*b#oZ zgM5^K5jp;4pN&}0=5`RF$C4kGrW2h*5^MK9FbHL-(P#0vt=-f!X})Fg-H|SYFz-*`6*H(4rb>^Z1 zt2j*&ctC|Qb(mGb>EoTTi`@$>_)@}|bpckQekZLeTcCRT6a1k0=ul&>%9y`jQ&U(o zQ|xMvsmq>qTNguEdMs`1=r|FI{bbQek8&TO)LBA7Jh*ux{Ebj`vo;aeE$efrF4s2h zoqUtqto-Em1#nD;ub$!*Q#v5WDsCGQs7VvSo(L9{pFb_p6v23An|ZM36H(!o<_#_7 ziz!mCCJOxOf^oHtcill3ysEYXzZH!*bQHCe>rfY-fbsy(Zi<*u9&qLecYW+uA=#8K zQqg}|hROTwa)t!9QPSJCF7kLeUC>+B+}B^Td+MGK-zp5deTzinO5nsRF8TKy0@S2Y zZm}e!3TE~Yu)|Ve9v9MLYEyaor>K4D6F)yZsM($9rg&|1CR?h+cI(um$9_D~cWToS z?x;)$O4YNN>x1B4bla$SGitHNA=#!eE2YsDQ~VrUjuO1~Pd_)LT2{Va_aP=_rTNyj z_pyeIo5F(3Fg|R3pFkawpL0BxgVeuB@2`5g_=5wV+lm9$F83Bkx^zuhYILNJNXyvW zIrW>#qegUMyYU;c+dVZJ>cK}R54E6uka1>p>}xC^ep2;-)-=Qub~aUKS%gP-rCzk@ zD@_#lH?NPFj<|62BC`$N+4!?~CUO-`mUVZ9F3ujUY;a z2tH2m9_dMh8=POYQdK9|`(Z!Pba5~H(Sg2M>P`cAs0$@hz70ehen&^Yn%?!kih1j_ zLz;b6*{}TLETI8{cA)K`#}}a0pB}S%dndulv&IjWQxJ&|3Si56tqa+VbI^{c?IZDH z){^vkMv^ku^fGmFy>Dpo=9x+OFw342mAy;B)`Xpqx8Fr~lsx&^?Tq6wwBzE0mT7U> z>R!pZJ7AU3%X7n*IJubG!7zUEQ4@A2K>@tZ;q@U@dqBzsxt$oFQ8{NBHfjV!y#0I5 zuNOOrplhSY*5Y1*J9B4rQekUzlByj#Mn!LO%PwEO<@w87!}-U??u0TVq?erFORg!v zy~Dr7TB2pP)mD;`X0?lF?3u8X%}WQzRIKxdq<%QiZ|vftLObUCPdL1a({{6@KY0y) z`r|e4&;IlgMg9ELShlQmB7Mb2kNkyOvh$w%8TV)d-Gc@8TOOU{(t5ZW^w__vKNi`s{*7Q%-amEZfpt&gUc^4>(bB?#Xvb;~+Va7x`N*jBb!WEQ@#6;mmwV5TcQ}C`XL{@$qQSr0pP4kc zEATvgtNu|$krCc^nUG;SFpS8w=_a1r|2dTxQ$E7KEn!(t02=e<7`uwz(ZATDp@MCS zZ9jN|>OPBd*#RdMrO!>OTxSy{Z_w%jIR|(UmOTuZc zBFFekqaeEoBAcEa_(i(m-En&;CS>$@$0;uaV@SJPcWVKvM@r^Ko`%JQsY=z!>L1w) zj5hKdf}vOTBINgEKwrq*goE*{7P@@7!~xG##&>2u(BO@jYk6 zxp@9q<}!0PeZ7M|rs}k8d>cT1aIb?2sf-1$1=JC&klNVTyUqC{z5WzZ3op+PQh5`m6hJe3-yyC0c6%8!$Pu)imcs~i& z1TQa*wI|I8fQG}eN9^eAV5;TgOLOaeQS#S3Y+Jr|9yn?UoB}UTLXNKEY@EG0L=GOC z-%Y=F2t+tN2HnwNA#jc%?2RQ@Qmn*<<)2W|pN;#$gvR)lEgbPaw8eP@nt5l~a-1KWn8Jbc$Z>xJ39vayDW9 zdryrMTQ3$Z^C;jOS2*ufSEtx1&)^m1=*M0^eSBi}N%zeXmzA3^2Z3q%YOOw-w*7@_ zq5RKF*M;x9HeD+z=DWrU$+f=wHOQ*)@%>vGA|as2(!EbMELPZe<#g4UbsawG*n(?Z zT3=ik{ly=5lhY7 zmb9Q`929$%YgCs)dA3J7)+FmmE!SfSuJg}c33C=RbPkYMUt+?O-NvOUoeu$PXl54e zt<`3;^d~Sry{J##Uk=D{bLidcmR)3IH&_;U9lxH}E3h$mvmd#w)`qK> zZ-0Hp@4@9HDxx^qKbF$ND$m@gh3v?O8Vtw zRVQ9PcoHjNhxEbEg?dJ#4d+x;sLSda39OWh#>NtW(4ruPS(5MK={3Mnj|2414Hk;y zEmz)hpu^*4XVtuWo3p*x>7)Imw-ftdA#PUGCKAxyo7vePLu+(X0RjRS4-Wg}*bj(C z%Y7a>$_S9+|oQ&~9fCh5FhNxG!TN411?cu}a(G0_%S z3hv6dRS`_7gZ)$48*Z{>d-RC;^e0PQ0NUU~H7Fhi?gUH!xi0t1 zaUKQhEg~1-_V#J6{j{FTBtK7lK-H97dl$l6F&o4KOx)$i|{sOb$CRSPPiWPM*TxN(U&^k-c7ucaGrb3*cNiMRYGN= zmSrY$MvQf?UzoUUJEy}ro1k<1`zfW58xXfHvG&M}KbQ9Z4NLRVKw81}F~db&?aeHv{a2i{_G?!Bo;_iC8G+@CqY{0>HVmXb{)LssKm@LVZ! z+x`Y-Zjd9Edb)4(yUqNGrk0BXT+eEv_*3pX3z%r|N2ooiwZNAy>9@(C7#bhNe%GI5 z@{_WTqdAyf76y?uP--kyM;famxEMc26^7re0py2`HC!D(@L3| z_q#EEU24vg#u}8yt5-O-i?Hl635#?mN#tL$GHh)fqo1BqBB(Z*4mhU=1c@YFY`Nn6 z5OkvjA%pP`1F>1?vN|k93j6df11`In_`JzYIy+Mz63$D}liAajdlM)Y|8m0P==Kcs z=z_}8|5uXq?^PF_YU4ml3UEev84H_^f3~eMILmW9>$cx1U3eM2lZl2vTa*0>%-SF# zaSFwal$1Cq6Z8%BzEWF2Yg;7#QNPlbEHa$UX~ti}JGFKMG2@I&X~Wa5Joy@G^>+70 zZ9slvw$eiK^V=T+=HU4oeTiqG{3|~t2MgIIe(>OT+7uCU4bh|a?>?Qd`%-ZE`Qu!N zB0qgPSp8s5e_Mm^_#Kxb3gR)<%gAI^ql)<4Y<<{0Pv)i}+7MS3?WLNjvz+spcfL2| z7|PvqclsJtw-Ifrhu&fFQkR-OKKYa5qt&(!uf!u_C9uA$F~U3jf7-b6a47e_Ke8U2 z$WAh4Nl`?}HW`tnq|vb(OpZOF99zXqIAx1zII@L=h@&Y>#2Cxi64|%GFeX`t8N`?w z(|dd0xAQ#jdp&>L*Zt>xUBCPH-9F3r{Qv!n|Jz0$+!6;8{3-9o& zRr$7?Q?BoXnpF*rjg8eBKW<}B(lugGKKo1|IS09u&oVMB}Mg6rH8jfuYwkDv%6|R{(dg;kMVK`JP3s{lv291;xaax^Z-e<=W9b7{`J|XY%pqW2EL}t>syy`W#+}t3(GGbLR#@&`Ojz`K)QD!M01_%XTde^P6dl zRb;pJv%BVV*=i#4f5f;&bZAO1=M+CDiO}A0n-8{i9g%6@Y?n0}xQF>T@HC^136Nv9 zp#=gEHGmwfSwjWRJYyD9o}^^1cgE|RMP_;1Wn5ZaGzK>NjDQYQe3CQliwrnjm!g?0 z-U8)k8@`FGa7!b>Yldr*eH$AB9)={Ji9OXCh{;wGo@mRod5}a_|NfaH5dE4_>a^da zW3AdU@A>yMTa`X?@emPqGPtJE-%9#9E8R0#y|zwcI1XbBES%Qg)m-o+2sYk?;LPypW8~#GnXj^(m zJhA_+;J9avB{FOOHPlCwp*p@6To{Q$Fjy@8sNZozIO~udIo*WqjRdK8%gk+zu`kQI zq-?Z(ht@ABOG)*6Sf)Xo)H7h?3Gam_qG{)R4gIwCQ7-50vsHd%V=cA>%DT=vlPSGE`?4TUk&EP6sfV zM9)plTqL>OSfO+vPobUOHx3n6WsH$X%8eg4z5S!&ough#DTi{7pW<%!!!B=J0lFS= zz;LdaLr|5NE;bs#YV6)=_^kovEZo2I|0d&XKfpTZkk)6+x zdn_HouTvn|wm(66eVF_#tN>78-6-`@n)i+3D`QS#%9Y^L6(R2v!z->C^4ZA<^O1>F z93_V0MytU_+7FOUB{4bISKjAzRky+7<1R=v|25DS@zmS+Hj>mB+qbfnym7cCNaL-H z;&C-?7z}5JtO4PrXr0)3>hEWdnmvAv*)z1uE5p6l;|HBmEd@D#nf;(i{e|2w>?e-w z2lH=uHlS5>ny$^HS-y4cs_7C3eL5r7p}5fE&@<=g&9dSQyNI*~%Aw+Bc{ZdLU9zw{ z&#_VqT6;l%D<#CII=zE*7q=>2_go+UdLCDGY|GGc0#u#Xdlw|DI$^^N5@C(b(|C(o5__|g%>lu3Y^g9>XSG}a zNBqUw$04kEf1>9J(JY|&$vjh}noNSUj){OPHX{oNh#FG5$WzXd70m7PE(Ce8rJLPV zZbX63Q5B^#2`SyM+*d3ce$33w^z3xg%+v;knZ6fZKCZYeOl>I+-H@X9e zmG%9X962O#4c)8hn)AK8;Y5c*+Exc%HfHVbZ%yee3nwXr_ixVtmMSWK=JX*wnyw_j z4vi|o^_VOa9brZDLBCqv{ms>Fa$@STzQ2o^c86RArk)x1MSXjmY|*JX88BbBHaZ8L z%-yM{mnLzfoT8LQaF@%L4A5~GvTTyAKHDxOEhr5IxCA!Lsn>2(%wh#+lK0!CP^SBWe2=p99l35+erXOw90sDzAR;MHBXMWYMx)=PH-`k}z&5QlmPdx966Vw~T(1dOf) z@+;ExzM>goWKo;1NzJ=qg&x0-cI{lNU<3Qy?x!YR3&(O(vay1|MDB?{%Z>(8OgIP7 zQ%F>zZvOxg(eu)103z*yTOGXwYjrkIohy@ zgi=GQMph6=hKx)sgVLrTUq=f^SInsNq2sAR#rXJZ$=!)^lNu#2A9bX{Y_pQIBRirKQo!>Sez!Q`SP+tQDE`qVUPsZrH=OTKbrao()VQ=#i!qYD*Lx2aX>>t|C481y@FPn z3UQvxyLQQuAiYJjk~U7$GsU2@B_9Rhd`m+9vf*Ea63|nbtx)kHs0e4!!DQSnQy^zF zM=!-`j%&xtkKQj|eXn}IoM~#uxc4CEjQbf{W!r70PW$hcL81Z+VIK9UqOaY}8ixhC zzZfL31zME&n$*c?AY2o{d||4icBNmb5vQoZA!)D&v1{V zR2=Z_IxL}ICTn{Gtst8WyVp8;AtRvTlU3*zWG12TZqZYA@pebF>?=51g%NkB4kd*1 zFLv#o3mnF37P?vSi0pZY2HP}m+%uLBl$;EJia_oG5P7b{P*5RSR_uaCS3Ke0~m#lNtTrR zp+s;(LF3I5IK-=EICSANbE`tWhml|L^U*%|%z0c6h&A)EVgZ^P%MskyWY zvShA5;t)A7!d+-{RaeWJ=jO3yjXUc~#Azy0=I%v0B8z0iD5cCM5_^fePru7*_~d0z z9?3q=oArK5YCicBg*zr@z%l<3s6|Gi<{!%U9|Q9N4)_1u+y|BD=c8_6J^DRRZXx)> z!;fp@!AlyShEi2?;9@D1fUKs8W1aO=)kFEi&6ALx)X4RkDC zUXY0*plCK!RmbRN8Mb033%@r&t{ct-uRXzZQ)VlX?t_56ssTo|U@0>4O~1hr4EtYN zI74{xXNIV9VW|5ksTBaYC!zNC(dXyCB@3tQ1`>%2vs*IY5+qk-(Z1t?vyt!*5GKEW z{|9K<7(Dn?4oWa&qr4cz&ZY3l06k7vWTYlRB?2ur75JS6sikkUZG6|-hqmF-@f--M zzZMj_D+eaza^=A~!yO2tmA-oHio?O;$R4J{2DKnLN3AC>kn=eN+tmyk!bdYQe}EK% zIm;aTyV!Z literal 0 HcmV?d00001 From 212d31e3416a4bf424ec8f5b243d70786f5a7b9e Mon Sep 17 00:00:00 2001 From: Tzvetan Mikov Date: Thu, 13 Nov 2025 21:10:10 -0800 Subject: [PATCH 5/5] README.md: add permalinks for custom widgets --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a21217a..2a64a5a 100644 --- a/README.md +++ b/README.md @@ -773,11 +773,11 @@ While you can compose existing React components together, Dear ImGui's real powe **How custom widgets work:** 1. **Add a render function** in [`lib/imgui-unit/renderer.js`](lib/imgui-unit/renderer.js) that uses ImGui's draw list API -2. **Add a case** to the switch statement in `renderNode()` +2. **Add a case** to the switch statement in [`renderNode()`](a86e558b6ea712179d955f04a6ec17493b18953a/lib/imgui-unit/renderer.js#L785) 3. **Use `_igDummy()`** to reserve layout space for your custom drawing 4. **Handle mouse interaction** using `_igGetMousePos()` and `_igIsMouseClicked_Bool()` -**Example: RadialMenu** ([`lib/imgui-unit/renderer.js:607-752`](lib/imgui-unit/renderer.js#L607-L752)) +**Example: RadialMenu** ([`lib/imgui-unit/renderer.js:607-752`](a86e558b6ea712179d955f04a6ec17493b18953a/lib/imgui-unit/renderer.js#L607-L752) ```javascript function renderRadialMenu(node: any, vec2: c_ptr): void {