Skip to content

Input system overhaul #332

@diabloproject

Description

@diabloproject

I am making this issue to discuss a possible way of implementing the next input system.
Right now, input system binds the shortcuts directly in code, which makes it hard to change for the end user, as well as creates some complications when adopting the editor to platforms with unusual terminal behaviours (e.g. macOS).

How input system can be implemented?

Constraints

As was already discussed here, inputs can:

  • Be declared outside of the main binary (see: extensions plan)
  • Work on condition (see: menubar)
  • Overlay each other (e.g. multiline textarea in modal and enter key)
    Additionally, following factors can influence the decision:
  • No callbacks by design
  • Immediate render mode
    So, what can be done? Solution that I came up with looks like this:

Overview

We already have two systems that will help us: Previous node tree and focus.
What I want to do, is to insert into node tree "Action scope boundaries", that will have scopes associated with them.
When the key is pressed, we are going to start at the node that is focused, and find the closest action boundary that has this shortcut/key bound for its scope, and fire the action associated with this shortcut.
On the render pass, we can return the action only when we are in the same scope again.
Code that is expected if this will be the chosen method:

fn setup(tui: &mut Tui) {
    tui.declare_scope("menu-a", &[("back", vk::ESC)]);
}


fn render_menu(ctx: &mut Context, state: &mut State) {
    ctx.scope_begin("menu-a", 12);
    // Normal rendering logic here...
    if ctx.consume_action("back") {
        state.menu_open = false;
    }
    ctx.scope_end();
}

Pros and cons

Pros:

  • Does not require any enums/hardcoding
  • Reasonably easy to create keymap overrides
  • Does not require callbacks, works reasonably well for immediate mode rendering

Cons:

  • Complicates consumption of input

Let me expand on that.

Complications

Let's define two UI elements:

fn list_render(ctx: &mut Context, state: &mut State) {
    ctx.scope_begin("list", 1);
    for i in 1..11 {
        checkbox_render(ctx, state, i);
    }
    ctx.scope_end();
}

fn checkbox_render(ctx: &mut Context, state: &mut State, n: usize) {
    ctx.scope_begin("checkbox", n)
    ctx.scope_end();
}

Let's assume that both "list" and "checkbox" scopes have DEL button bound. For checkbox, DEL deletes the checkbox sometimes (for now let's say 50% of the time), for list it deletes the list itself (always).
Let's consider this state (I will write it in xml, just for ease of understanding), including action boundaries (tag asb):

<asb scope="list" key=1>
    <list>
        <asb scope="checkbox" key=1>
            <checkbox/>
        </asb>
    </list>
</asb>

Human pressed DEL, with the focus on the checkbox, and the checkbox have not been deleted. That means that now we need to propagate the action up, because it's not handled. But, for the system to give you back DEL bound to the "list" scope, it needs to either:

  • Be sure that checkbox with key 1 did not handle the key
  • checkbox with key 1 never appeared in the render sequence

First one is easy to verify, but No. 2 cannot be guaranteed unless we closed the scope. So the only solution it to only allow developers to put key consumption AFTER all the children were rendered, and panic! if someone tried to first consume the key and then the child scope we were targeting with focus appeared down the render chain.
When I thought of this, I reasoned that this will make hiding/deleting elements harder (sometimes).

Well, this is not only option. But the second option requires us to be able to abandon rerenders, which makes everything much more complicated and probably undesirable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    I-featureFeature requests and any other major tasks. Requires major work in the 1000s of LOC.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions