handmade.network » Forums » Experimenting with an IM-GUI style api for rendering web pages (DOM)
hasen.judy
6 posts
I'm making a mobile application for the Ryoukai Japanese-Arabic dictionary.
#16524 Experimenting with an IM-GUI style api for rendering web pages (DOM)
1 week, 2 days ago Edited by hasen.judy on Oct. 10, 2018, 11:09 p.m. Reason: Initial post

I've been conducting an experiment to try to find an answer to this question:

Is it possible to program a webpage UI in an IM-GUI style?


The things I wanted the most were (in terms of API, not necessarily in terms of what actually happens):

- Render an element and immediately query its state.

- Handle events without separate callbacks - it's just part of querying the element state.

- Store/associate arbitrary state with any DOM element.


I've only started this a couple weeks on the side so it's very much just a prototype.

The kind of API I'm coming up with looks sort of like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    create element, "div"
        set some styles and attributes
        if this element is clicked:
             print console message
        get the custom state "hover" { is_inside: boolean; time_entered: number }
        if element is under mouse but not hover.is_inside
              hover.is_inside = true
              hover.time_entered = now
        if element is under mouse and hover.is_inside:
              time_inside = now - hover.time_entered
              if time_inside > 100ms:
                   show tool tip
              append text element: "hover time " + time_inside + "ms"


It might be verbose. I don't know if this is a good way to write the UI. So it's just a prototype, but it basically works for a small test case.

The trick is I create a "virtual dom" where each element has a "key" to track its position within the parent. On first render of any element we don't have any information about the element because it's not rendered yet, but on the next frame we can use the key to find the real backing dom element and get its location on the screen and compare it with the position of the mouse in this frame, etc.

The real version of the above pseudo code actually looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
    im.vnode("div", "hover-test", function() {
        im.attr("style", "position: absolute; left: 140; top: 160; width: 200px; height: 70px; padding: 10px 12px; margin: 10px 4px; display: block; border-radius: 3px; border: 1px solid silver; background: whitesmoke");
        im.vnode("div", "title", function() {
            im.text("Hover me!<br>")
        })

        let state = compute_hover_state(im)
        render_tooltip(im, state, "You are hovering!")
        if (state.is_inside) {
            im.vnode("div", "info", function() {
                im.text("Hover duration: ");
                im.text((state.time_inside/1000).toFixed(1));
            })
        }
    });


// supporting code; in other parts of the file:


class Hover_State {
    is_inside: boolean = false;
    time_entered: number = 0;
    time_inside: number = 0;
    time_left: number = 0;
    time_outside: number = 0;

    static make(): Hover_State {
        return new Hover_State();
    }
}


function compute_hover_state(im: Builder_Context): Hover_State {
    let state = im.get_state(Hover_State.make, "hover");
    if (im.is_under_mouse())
    {
        if (state.is_inside)
        {
            state.time_inside = current_time_ms() - state.time_entered;
        }
        else
        {
            state.is_inside = true;
            state.time_entered = current_time_ms();
            state.time_inside = 0;
        }
    }
    else
    {
        if (!state.is_inside) { // already outisde, increment time
            state.time_outside = current_time_ms() - state.time_left;
        } else {
            state.is_inside = false;
            state.time_left = current_time_ms();
            state.time_outside = 0;
        }
    }
    return state;
}

function render_tooltip(im: Builder_Context, state: Hover_State, message: string) {
    const delay = 200; // in ms
    const fade = 200; // in ms
    let mouse = im.ui.mouse.pos;
    if (state.time_inside > delay && (state.is_inside || state.time_outside < fade)) {
        let opacity = state.is_inside ? 1 : 0;
        im.vnode("div", "hover_tooltip", function() {
            let tool_el = im.get_dom_element();
            let width = 0;
            let height = 0;
            if (tool_el) {
                width = tool_el.getBoundingClientRect().width;
                height = tool_el.getBoundingClientRect().height;
            } else { // this is the first frame
                // Setting opacity to 0 on the first frame achieves two things:
                // 1) hide first frame to avoid positioning flicker (position not yet well known)
                // 2) achieve a fade in effect by using `transition: opacity <duration>` css
                opacity = 0;
            }
            let left = Math.floor(mouse.x - width / 2); // center
            let top = Math.floor(mouse.y - height - 10); // space above cursor
            im.attr("style", `transition: opacity ${fade}ms; position: fixed; opacity: ${opacity}; left: ${left}; top: ${top}; background: hsla(0, 0%, 0%, 0.8); border-radius: 2px; color: white; font: normal 10pt sans-serif; padding: 6px 9px;`);
            im.text(message);
        })
    }
}


This is code that actually works and detects hover state and displays a tooltip thing when the mouse is over the element in question.

If anyone is interested, for now I'm pushing the code to https://git.sr.ht/~hasen/imdom/tree/master/src/vdom.ts

It's typescript so needs tsc (which needs npm) to run. If you want to try it, just open 'page.html' in chrome after running 'tsc' on the command line.
mmozeiko
Mārtiņš Možeiko
1811 posts / 1 project
#16525 Experimenting with an IM-GUI style api for rendering web pages (DOM)
1 week, 2 days ago Edited by Mārtiņš Možeiko on Oct. 11, 2018, 8:25 a.m.

hasen.judy
Is it possible to program a webpage UI in an IM-GUI style?


Yes, it is possible and that is called React, vue.js and similar frameworks. They do imgui style "rendering" for page structure.
hasen.judy
6 posts
I'm making a mobile application for the Ryoukai Japanese-Arabic dictionary.
#16531 Experimenting with an IM-GUI style api for rendering web pages (DOM)
1 week, 2 days ago

I'm aware of React, but I don't like their approach.

React has components (as objects) with a lifecycle, and you have to respond to events to update state in various ways. Components have an explicit "state" and "props" and they interact in weird ways (props can't be updated, but state can, but props can actually be updated from outside). It also doesn't play very nice with forms because React wants to pretend that its virtual dom is the single source of truth.

The "immediate mode" style that I care about here is being able to "render" any element as a function call.

There's no way to do this with React:

1
2
3
4
if (ImGui::Button("Save"))
{
    // do stuff
}


or this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if (ImGui::BeginMenuBar())
{
    if (ImGui::BeginMenu("File"))
    {
        if (ImGui::MenuItem("Open..", "Ctrl+O")) { /* Do stuff */ }
        if (ImGui::MenuItem("Save", "Ctrl+S"))   { /* Do stuff */ }
        if (ImGui::MenuItem("Close", "Ctrl+W"))  { my_tool_active = false; }
        ImGui::EndMenu();
    }
    ImGui::EndMenuBar();
}


I'm not necessarily doing that either, but I'm trying to make it possible to do things that way. (dear imgui is limited in the way you can customize the appearance as far as I can tell).

Props? They are just parameters you can pass to the function.

State? You can associate arbitrary data with any element.