zmkfirmware / zmk

ZMK Firmware Repository
https://zmk.dev/
MIT License
2.73k stars 2.78k forks source link

Feature request: Conditional layer support (including tri-layer for lower/raise/adjust) #828

Closed bcat closed 2 years ago

bcat commented 3 years ago

I was thinking of simple ZMK features I could hack on that I'd actually use, and tri-layer support (like QMK and BlueMicro firmware offer) came to the top of my list. You can fake it today by nesting momentary layer mappings, but the layer deactivation behavior on layer up isn't the same. (For example, if you have press Lower, press Raise, release Lower, you stay on the "adjust" layer until Raise is released. Real tri-layer support would take you back to the "raise" layer at this point.)

I thought about implementing this feature using new layer behaviors, or about integrating with the combo system somehow, but the cleanest approach I could come up with actually involves a new node next to the keymap like so:

#define LOWER 1
#define RAISE 2
#define ADJUST 3

multi_layers {
  compatible = "zmk,multi-layers";
  adjust {
    if-layers = <LOWER RAISE>;
    then-layer = ADJUST;
  };
};

keymap {
  compatible = "zmk,keymap";
  default_layer {
    &kp A &kp B
    &mo LOWER &mo RAISE
  } 
  lower_layer {
    &kp C &kp D
    &trans &trans
  }
  raise_layer {
    &kp E &kp F
    &trans &trans
  }
  adjust_layer {
    &kp G &kp H
    &trans &trans
  }
};

I wrote a first draft implementation (that I've not tested much yet), and I'd like to get some early feedback in two areas. (Of course, it's also possible this approach is wrong on a more fundamental level. That feedback is welcome as well. 😄)

First, should multi-layer configs be applied recursively? In other words, should activating the then-layer of one config be able to trigger the if-layer of another config?

Advantages to recursive activation:

Disadvantages to recursive activation:

Second, should multi-layer (de)activation update the existing layer state bitset, or should there be a new bitset for multi-layer activations that gets OR'd with the existing layer state bitset to compute the "effective layers state"?

Advantages to updating the existing layers state:

Disadvantages to updating the existing layers state:

bcat commented 3 years ago

Regarding OR'ing multiple layer state bitsets together to compute the effective layer state, I feel like my natural inclination would be allowing code to register a new layer state bitset, so that e.g. keymap.c could register one for the layer behaviors, multi_layer.c could register one for the multi-layer configs, and if users wanted to even more customer layer logic, they could register their own bitset in a C file in their ZMK config repo. (Assuming that's a thing you can do today; I don't actually know if it's possible to put custom source code into a config repo.) But this may be a bit overdesigned... 😄

bcat commented 3 years ago

I guess another alternative solution to the "same layer activated from multiple places simultaneously" problem would be to store an activation count for each layer instead of just an is active bit. But in addition to maybe being worse from a storage perspective, I always find these type of ref-counting style approaches to be hard to reason about the correctness of when reading the code.

bcat commented 3 years ago

Also, now that I think about it, something like "conditional layers" would probably be a clearer name. But it's tricky because the community calls the Lower + Raise = Adjust version "tri layer" and deviating too much from that name might else be confusing. IDK, naming is hard....

bcat commented 3 years ago

Thinking about this a little bit more:

  1. I really like the idea of calling this feature "conditional layers" or "layer combos" or something, and simply documenting that you can achieve "tri layer" support using it. The more I look at the time "multi layer" the less I like it. :)

  2. I wonder if the "then-layer" attribute should just specify bindings like the combos feature uses. Instead of then-layer = ADJUST, you'd write binds = <&mo ADJUST>, for instance. It's more flexible and sidesteps the question about one vs. many active layer sets. (But also it may be overkill.... I can't imagine ever using a behavior other than &mo here.)

WDYT?

bcat commented 3 years ago

Getting back to this, I am renaming the feature to "conditional layers" since others seemed to think that name was clearer and I agree.

I'm probably punting on the more flexible binding support; we'll see.

evantravers commented 3 years ago

I've been trying to figure out if you could do this with the existing combo functionality… is it possible to something like this pseudocode:

/ {
    combos {
        compatible = "zmk,combos";
        combo_esc {
            timeout-ms = <50>;
            key-positions = <0 1>;
            bindings = <&mo ADJUST>;
        };
    };
};
petejohanson commented 3 years ago

I've been trying to figure out if you could do this with the existing combo functionality… is it possible to something like this pseudocode:

/ {
    combos {
        compatible = "zmk,combos";
        combo_esc {
            timeout-ms = <50>;
            key-positions = <0 1>;
            bindings = <&mo ADJUST>;
        };
    };
};

Yes, this is the current workaround using combos. The main downside is the timing of pressing them at the same time is required when using combos. See a working example at https://github.com/petejohanson/zaphod-config/blob/main/boards/arm/zaphod/zaphod.keymap#L25

bcat commented 3 years ago

Yup, there are two workarounds I'm aware of, neither of which is completely satisfactory:

  1. On the LOWER layer, rebind the RAISE position to &mo ADJUST and on the RAISE layer, rebind the LOWER position to &mo ADJUST. The problem with this approach is that interleaved press and release doesn't work as expected. If you press LOWER, press RAISE, release LOWER, you'll stay on the ADJUST layer, whereas a "real" tri-layer setup will take you to the RAISE layer.
  2. The combo workaround that Pete mentioned, which has the timing constraint that Pete also mentioned. If you press LOWER, wait a second, then press RAISE, the combo won't trigger (unless you have a very long combo timeout).
evantravers commented 3 years ago

Thanks @petejohanson and @bcat. I think that I'll be ok with the combo workaround for now… my "adjust" layer is pretty intentional, reset, volume, etc. I don't really flow into it from one layer to another.