Custom Modals

ESDK provides two complementary modal systems: HTML modals that open as real native OS windows, and native system dialogs called from Python. Both replace the default browser alert() / prompt() / confirm() with styled, controllable alternatives.

1. HTML Modals (Frontend)

HTML modals are template files stored in ui/modals/. When opened, the engine spawns a native OS window on its own thread — completely separate from the main app window. The modal can have a custom HTML titlebar (no OS chrome) or the standard OS titlebar, controlled by the <modal> and <titlebar> elements.

Calling a Modal from JavaScript

// Returns a Promise that resolves when the modal calls closeModal()
window.openModal('alert').then(function (result) {
    console.log('Modal closed with:', result);
});

// Or with async/await
const result = await window.openModal('prompt');
if (result !== null) {
    console.log('User entered:', result);
}

Template File Format

Each modal lives in ui/modals/{name}.html. The outer <modal> element controls the OS window; inner sections define the content:

<modal width="420" height="240" blur="true" closeable="true" titlebar="true">

    <!-- Custom HTML titlebar (value="false" = no OS chrome) -->
    <titlebar value="false" height="40">
        <div style="background:#111; display:flex; align-items:center; padding:0 14px; height:100%; color:#fff;">
            My Dialog
            <button onclick="Close()" style="margin-left:auto;">&times;</button>
        </div>
    </titlebar>

    <!-- OS native titlebar (value="true") -->
    <!-- <titlebar value="true"></titlebar> -->

    <title><value>My Dialog</value></title>

    <content>
        <div style="padding:28px; text-align:center; color:#fff;">
            <h2>Hello!</h2>
            <button onclick="closeModal('ok')">OK</button>
        </div>
    </content>

    <script>
        function Close() { closeModal(null); }
    </script>
</modal>

<modal> attributes

AttributeTypeDescription
widthnumberWindow width in logical pixels
heightnumberWindow height in logical pixels
blurbooleanDims and blurs the parent window while the modal is open
closeablebooleanWhether the OS close button is enabled (native titlebar only)
titlebarbooleanPresence of any titlebar (native or custom)

<titlebar> attributes

AttributeTypeDescription
value"false" / "true"false = custom HTML titlebar, no OS chrome; true = OS native titlebar
heightnumberHeight of the custom titlebar drag region in pixels (only when value="false")

Rounded Corners

Frameless modals (custom titlebar) automatically inherit the CORNER_RADIUS value from ui/modals/properties.config. Set it there to apply rounded corners to all frameless modal windows.

# ui/modals/properties.config
CORNER_RADIUS=20

Built-in Modals

FilePurposeTitlebarReturns
alert.htmlSimple dismissal dialogCustom HTMLnull
prompt.htmlText input dialogOS nativestring or null
custom.htmlBlank starter templateOS nativetrue / false

Closing a Modal

Call closeModal(value) from inside a modal's <script> block. The value is forwarded to the parent's Promise. Pass null to indicate cancellation.

function Submit() {
    var input = document.getElementById('modal-input').value;
    closeModal(input);   // resolves with the user's text
}
function Cancel() {
    closeModal(null);    // resolves with null
}

Creating a Custom Modal

Duplicate ui/modals/custom.html, rename it, and update the sections. No C++ changes are needed.

<!-- ui/modals/confirm-delete.html -->
<modal width="400" height="200" blur="true" closeable="false">
    <titlebar value="true"></titlebar>
    <title><value>Confirm Delete</value></title>
    <content>
        <div style="padding:28px; text-align:center; color:#fff; font-family:system-ui,sans-serif;">
            <h2>Delete Item?</h2>
            <p style="color:#9ca3af">This action cannot be undone.</p>
            <button onclick="closeModal(false)">Cancel</button>
            <button onclick="closeModal(true)">Delete</button>
        </div>
    </content>
    <script></script>
</modal>
// Calling your custom modal
const confirmed = await window.openModal('confirm-delete');
if (confirmed) deleteItem();

2. Native System Dialogs (Python Backend)

The public/modals.py module wraps tkinter system dialogs. These are native OS windows — not WebView — so they work even before the main window is ready, during background tasks, or whenever you need the OS to render the dialog. Each call blocks the calling thread until the user dismisses the dialog, then returns the result synchronously.

Importing

from public import modals

Function Reference

FunctionParametersReturnsDialog type
show_alert(title, message) title: str, message: str None Warning — single OK button
show_info(title, message) title: str, message: str None Information — single OK button
show_error(title, message) title: str, message: str None Error — single OK button
show_confirm(title, message) title: str, message: str bool Yes / No — returns True if Yes
show_prompt(title, message, default="") title: str, message: str, default: str str | None Text input — returns the string, or None if cancelled

show_alert

Shows a native OS warning dialog. Blocks until the user clicks OK.

modals.show_alert("Low Disk Space", "Less than 500 MB remaining. Please free up space.")

show_info

Shows a native OS information dialog. Blocks until the user clicks OK.

modals.show_info("Export Complete", "Your file has been saved to the Downloads folder.")

show_error

Shows a native OS error dialog. Blocks until the user clicks OK.

modals.show_error("Connection Failed", "Could not reach the server. Check your network and try again.")

show_confirm

Shows a native OS Yes / No dialog. Returns True if the user clicks Yes, False if they click No.

confirmed = modals.show_confirm("Delete Item", "This action cannot be undone. Continue?")
if confirmed:
    delete_item()

show_prompt

Shows a native OS text-input dialog. Returns the entered string, or None if the user cancels. Supply default to pre-fill the input field.

name = modals.show_prompt("Rename File", "Enter a new name:", default="untitled")
if name is not None:
    rename_file(name)

Calling from the Bridge

Register an action in server/api.py to expose any modal to the JavaScript frontend. The bridge Promise resolves once the user dismisses the dialog and the Python thread unblocks.

Python — server/api.py

from public import modals

def handle_message(message_str):
    req    = json.loads(message_str)
    action = req.get("action")

    # Blocking alert — resolves with null once dismissed
    if action == "modal_alert":
        modals.show_alert(req.get("title", "Alert"), req.get("message", ""))
        return json.dumps({"status": "ok", "result": None})

    # Blocking info dialog
    elif action == "modal_info":
        modals.show_info(req.get("title", "Info"), req.get("message", ""))
        return json.dumps({"status": "ok", "result": None})

    # Blocking error dialog
    elif action == "modal_error":
        modals.show_error(req.get("title", "Error"), req.get("message", ""))
        return json.dumps({"status": "ok", "result": None})

    # Yes/No confirm — result is true or false
    elif action == "modal_confirm":
        confirmed = modals.show_confirm(req.get("title", "Confirm"), req.get("message", ""))
        return json.dumps({"status": "ok", "result": confirmed})

    # Text prompt — result is the string, or null if cancelled
    elif action == "modal_prompt":
        value = modals.show_prompt(req.get("title", "Input"), req.get("message", ""), req.get("default", ""))
        return json.dumps({"status": "ok", "result": value})

JavaScript — calling from the UI

// Alert — resolves once dismissed
await window.invokeBridge({ action: 'modal_alert', title: 'Warning', message: 'Disk space is low.' });

// Info
await window.invokeBridge({ action: 'modal_info', title: 'Done', message: 'Export complete.' });

// Error
await window.invokeBridge({ action: 'modal_error', title: 'Error', message: 'Connection failed.' });

// Confirm — result.result is true (Yes) or false (No)
const { result: confirmed } = await window.invokeBridge({
    action: 'modal_confirm',
    title: 'Delete Item',
    message: 'This cannot be undone. Continue?'
});
if (confirmed) deleteItem();

// Prompt — result.result is the entered string, or null if cancelled
const { result: name } = await window.invokeBridge({
    action: 'modal_prompt',
    title: 'Rename',
    message: 'Enter a new name:',
    default: 'untitled'
});
if (name !== null) renameItem(name);

Blocking Behaviour

Every function in modals.py blocks the Python thread until the dialog is dismissed. This means:

  • The bridge request will not return until the user responds — the JavaScript await naturally waits for it.
  • No other bridge calls can be processed on that thread while a dialog is open.
  • Do not call modals from a background thread that owns resources locked by the main thread, as this can cause a deadlock.

When to Use Each System

HTML ModalsPython (tkinter) Dialogs
Triggered fromJavaScript frontendPython backend or via bridge
AppearanceFully custom (CSS)Native OS look & feel
Return valueVia Promise (closeModal)Synchronous Python return
Works before webview loadsNoYes
Best forIn-app UX flows, styled dialogsBackend errors, startup checks, background tasks