Shawn Meier
3 years ago
committed by
Julia Luna
41 changed files with 1351 additions and 14 deletions
@ -0,0 +1,9 @@ |
|||||||
|
/ { |
||||||
|
behaviors { |
||||||
|
/omit-if-no-ref/ mkp: behavior_mouse_key_press { |
||||||
|
compatible = "zmk,behavior-mouse-key-press"; |
||||||
|
label = "MOUSE_KEY_PRESS"; |
||||||
|
#binding-cells = <1>; |
||||||
|
}; |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,12 @@ |
|||||||
|
/ { |
||||||
|
behaviors { |
||||||
|
/omit-if-no-ref/ mmv: behavior_mouse_move { |
||||||
|
compatible = "zmk,behavior-mouse-move"; |
||||||
|
label = "MOUSE_MOVE"; |
||||||
|
#binding-cells = <1>; |
||||||
|
delay-ms = <0>; |
||||||
|
time-to-max-speed-ms = <300>; |
||||||
|
acceleration-exponent = <1>; |
||||||
|
}; |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,12 @@ |
|||||||
|
/ { |
||||||
|
behaviors { |
||||||
|
/omit-if-no-ref/ mwh: msc: behavior_mouse_scroll { |
||||||
|
compatible = "zmk,behavior-mouse-scroll"; |
||||||
|
label = "MOUSE_SCROLL"; |
||||||
|
#binding-cells = <1>; |
||||||
|
delay-ms = <0>; |
||||||
|
time-to-max-speed-ms = <300>; |
||||||
|
acceleration-exponent = <0>; |
||||||
|
}; |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,5 @@ |
|||||||
|
description: Mouse key press/release behavior |
||||||
|
|
||||||
|
compatible: "zmk,behavior-mouse-key-press" |
||||||
|
|
||||||
|
include: one_param.yaml |
@ -0,0 +1,13 @@ |
|||||||
|
description: Mouse move |
||||||
|
|
||||||
|
compatible: "zmk,behavior-mouse-move" |
||||||
|
|
||||||
|
include: one_param.yaml |
||||||
|
|
||||||
|
properties: |
||||||
|
delay-ms: |
||||||
|
type: int |
||||||
|
time-to-max-speed-ms: |
||||||
|
type: int |
||||||
|
acceleration-exponent: |
||||||
|
type: int |
@ -0,0 +1,13 @@ |
|||||||
|
description: Mouse scroll |
||||||
|
|
||||||
|
compatible: "zmk,behavior-mouse-scroll" |
||||||
|
|
||||||
|
include: one_param.yaml |
||||||
|
|
||||||
|
properties: |
||||||
|
delay-ms: |
||||||
|
type: int |
||||||
|
time-to-max-speed-ms: |
||||||
|
type: int |
||||||
|
acceleration-exponent: |
||||||
|
type: int |
@ -0,0 +1,55 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
#pragma once |
||||||
|
|
||||||
|
/* Mouse press behavior */ |
||||||
|
/* Left click */ |
||||||
|
#define MB1 (0x01) |
||||||
|
#define LCLK (MB1) |
||||||
|
|
||||||
|
/* Right click */ |
||||||
|
#define MB2 (0x02) |
||||||
|
#define RCLK (MB2) |
||||||
|
|
||||||
|
/* Middle click */ |
||||||
|
#define MB3 (0x04) |
||||||
|
#define MCLK (MB3) |
||||||
|
|
||||||
|
#define MB4 (0x08) |
||||||
|
|
||||||
|
#define MB5 (0x10) |
||||||
|
|
||||||
|
#define MB6 (0x20) |
||||||
|
|
||||||
|
#define MB7 (0x40) |
||||||
|
|
||||||
|
#define MB8 (0x80) |
||||||
|
|
||||||
|
/* Mouse move behavior */ |
||||||
|
#define MOVE_VERT(vert) ((vert)&0xFFFF) |
||||||
|
#define MOVE_VERT_DECODE(encoded) (int16_t)((encoded)&0x0000FFFF) |
||||||
|
#define MOVE_HOR(hor) (((hor)&0xFFFF) << 16) |
||||||
|
#define MOVE_HOR_DECODE(encoded) (int16_t)(((encoded)&0xFFFF0000) >> 16) |
||||||
|
|
||||||
|
#define MOVE(hor, vert) (MOVE_HOR(hor) + MOVE_VERT(vert)) |
||||||
|
|
||||||
|
#define MOVE_UP MOVE_VERT(-600) |
||||||
|
#define MOVE_DOWN MOVE_VERT(600) |
||||||
|
#define MOVE_LEFT MOVE_HOR(-600) |
||||||
|
#define MOVE_RIGHT MOVE_HOR(600) |
||||||
|
|
||||||
|
/* Mouse scroll behavior */ |
||||||
|
#define SCROLL_VERT(vert) ((vert)&0xFFFF) |
||||||
|
#define SCROLL_VERT_DECODE(encoded) (int16_t)((encoded)&0x0000FFFF) |
||||||
|
#define SCROLL_HOR(hor) (((hor)&0xFFFF) << 16) |
||||||
|
#define SCROLL_HOR_DECODE(encoded) (int16_t)(((encoded)&0xFFFF0000) >> 16) |
||||||
|
|
||||||
|
#define SCROLL(hor, vert) (SCROLL_HOR(hor) + SCROLL_VERT(vert)) |
||||||
|
|
||||||
|
#define SCROLL_UP SCROLL_VERT(10) |
||||||
|
#define SCROLL_DOWN SCROLL_VERT(-10) |
||||||
|
#define SCROLL_LEFT SCROLL_HOR(-10) |
||||||
|
#define SCROLL_RIGHT SCROLL_HOR(10) |
@ -0,0 +1,27 @@ |
|||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <zephyr.h> |
||||||
|
#include <zmk/hid.h> |
||||||
|
#include <zmk/event_manager.h> |
||||||
|
#include <zmk/mouse.h> |
||||||
|
|
||||||
|
struct zmk_mouse_button_state_changed { |
||||||
|
zmk_mouse_button_t buttons; |
||||||
|
bool state; |
||||||
|
int64_t timestamp; |
||||||
|
}; |
||||||
|
|
||||||
|
ZMK_EVENT_DECLARE(zmk_mouse_button_state_changed); |
||||||
|
|
||||||
|
static inline struct zmk_mouse_button_state_changed_event * |
||||||
|
zmk_mouse_button_state_changed_from_encoded(uint32_t encoded, bool pressed, int64_t timestamp) { |
||||||
|
return new_zmk_mouse_button_state_changed((struct zmk_mouse_button_state_changed){ |
||||||
|
.buttons = HID_USAGE_ID(encoded), .state = pressed, .timestamp = timestamp}); |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <zephyr.h> |
||||||
|
#include <zmk/event_manager.h> |
||||||
|
#include <zmk/mouse.h> |
||||||
|
|
||||||
|
struct zmk_mouse_move_state_changed { |
||||||
|
struct vector2d max_speed; |
||||||
|
struct mouse_config config; |
||||||
|
bool state; |
||||||
|
int64_t timestamp; |
||||||
|
}; |
||||||
|
|
||||||
|
ZMK_EVENT_DECLARE(zmk_mouse_move_state_changed); |
||||||
|
|
||||||
|
static inline struct zmk_mouse_move_state_changed_event * |
||||||
|
zmk_mouse_move_state_changed_from_encoded(uint32_t encoded, struct mouse_config config, |
||||||
|
bool pressed, int64_t timestamp) { |
||||||
|
struct vector2d max_speed = (struct vector2d){ |
||||||
|
.x = MOVE_HOR_DECODE(encoded), |
||||||
|
.y = MOVE_VERT_DECODE(encoded), |
||||||
|
}; |
||||||
|
|
||||||
|
return new_zmk_mouse_move_state_changed((struct zmk_mouse_move_state_changed){ |
||||||
|
.max_speed = max_speed, .config = config, .state = pressed, .timestamp = timestamp}); |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <zephyr.h> |
||||||
|
#include <zmk/event_manager.h> |
||||||
|
#include <zmk/mouse.h> |
||||||
|
#include <dt-bindings/zmk/mouse.h> |
||||||
|
|
||||||
|
struct zmk_mouse_scroll_state_changed { |
||||||
|
struct vector2d max_speed; |
||||||
|
struct mouse_config config; |
||||||
|
bool state; |
||||||
|
int64_t timestamp; |
||||||
|
}; |
||||||
|
|
||||||
|
ZMK_EVENT_DECLARE(zmk_mouse_scroll_state_changed); |
||||||
|
|
||||||
|
static inline struct zmk_mouse_scroll_state_changed_event * |
||||||
|
zmk_mouse_scroll_state_changed_from_encoded(uint32_t encoded, struct mouse_config config, |
||||||
|
bool pressed, int64_t timestamp) { |
||||||
|
struct vector2d max_speed = (struct vector2d){ |
||||||
|
.x = SCROLL_HOR_DECODE(encoded), |
||||||
|
.y = SCROLL_VERT_DECODE(encoded), |
||||||
|
}; |
||||||
|
|
||||||
|
return new_zmk_mouse_scroll_state_changed((struct zmk_mouse_scroll_state_changed){ |
||||||
|
.max_speed = max_speed, .config = config, .state = pressed, .timestamp = timestamp}); |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <dt-bindings/zmk/mouse.h> |
||||||
|
#include <zephyr.h> |
||||||
|
#include <zmk/event_manager.h> |
||||||
|
#include <zmk/mouse.h> |
||||||
|
|
||||||
|
struct zmk_mouse_tick { |
||||||
|
struct vector2d max_move; |
||||||
|
struct vector2d max_scroll; |
||||||
|
struct mouse_config move_config; |
||||||
|
struct mouse_config scroll_config; |
||||||
|
int64_t *start_time; |
||||||
|
int64_t timestamp; |
||||||
|
}; |
||||||
|
|
||||||
|
ZMK_EVENT_DECLARE(zmk_mouse_tick); |
||||||
|
|
||||||
|
static inline struct zmk_mouse_tick_event *zmk_mouse_tick(struct vector2d max_move, |
||||||
|
struct vector2d max_scroll, |
||||||
|
struct mouse_config move_config, |
||||||
|
struct mouse_config scroll_config, |
||||||
|
int64_t *movement_start) { |
||||||
|
return new_zmk_mouse_tick((struct zmk_mouse_tick){ |
||||||
|
.max_move = max_move, |
||||||
|
.max_scroll = max_scroll, |
||||||
|
.move_config = move_config, |
||||||
|
.scroll_config = scroll_config, |
||||||
|
.start_time = movement_start, |
||||||
|
.timestamp = k_uptime_get(), |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <zephyr.h> |
||||||
|
#include <dt-bindings/zmk/mouse.h> |
||||||
|
|
||||||
|
typedef uint16_t zmk_mouse_button_flags_t; |
||||||
|
typedef uint16_t zmk_mouse_button_t; |
||||||
|
|
||||||
|
struct mouse_config { |
||||||
|
int delay_ms; |
||||||
|
int time_to_max_speed_ms; |
||||||
|
// acceleration exponent 0: uniform speed
|
||||||
|
// acceleration exponent 1: uniform acceleration
|
||||||
|
// acceleration exponent 2: uniform jerk
|
||||||
|
int acceleration_exponent; |
||||||
|
}; |
||||||
|
|
||||||
|
struct vector2d { |
||||||
|
float x; |
||||||
|
float y; |
||||||
|
}; |
||||||
|
|
||||||
|
struct k_work_q *zmk_mouse_work_q(); |
||||||
|
int zmk_mouse_init(); |
@ -0,0 +1,48 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#define DT_DRV_COMPAT zmk_behavior_mouse_key_press |
||||||
|
|
||||||
|
#include <device.h> |
||||||
|
#include <drivers/behavior.h> |
||||||
|
#include <logging/log.h> |
||||||
|
|
||||||
|
#include <zmk/behavior.h> |
||||||
|
#include <zmk/event_manager.h> |
||||||
|
#include <zmk/events/mouse_button_state_changed.h> |
||||||
|
|
||||||
|
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); |
||||||
|
|
||||||
|
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) |
||||||
|
|
||||||
|
static int behavior_mouse_key_press_init(const struct device *dev) { return 0; }; |
||||||
|
|
||||||
|
static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, |
||||||
|
struct zmk_behavior_binding_event event) { |
||||||
|
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); |
||||||
|
|
||||||
|
return ZMK_EVENT_RAISE( |
||||||
|
zmk_mouse_button_state_changed_from_encoded(binding->param1, true, event.timestamp)); |
||||||
|
} |
||||||
|
|
||||||
|
static int on_keymap_binding_released(struct zmk_behavior_binding *binding, |
||||||
|
struct zmk_behavior_binding_event event) { |
||||||
|
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); |
||||||
|
return ZMK_EVENT_RAISE( |
||||||
|
zmk_mouse_button_state_changed_from_encoded(binding->param1, false, event.timestamp)); |
||||||
|
} |
||||||
|
|
||||||
|
static const struct behavior_driver_api behavior_mouse_key_press_driver_api = { |
||||||
|
.binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; |
||||||
|
|
||||||
|
#define KP_INST(n) \ |
||||||
|
DEVICE_DT_INST_DEFINE(n, behavior_mouse_key_press_init, device_pm_control_nop, NULL, NULL, \
|
||||||
|
APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \
|
||||||
|
&behavior_mouse_key_press_driver_api); |
||||||
|
|
||||||
|
DT_INST_FOREACH_STATUS_OKAY(KP_INST) |
||||||
|
|
||||||
|
#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ |
@ -0,0 +1,57 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#define DT_DRV_COMPAT zmk_behavior_mouse_move |
||||||
|
|
||||||
|
#include <device.h> |
||||||
|
#include <drivers/behavior.h> |
||||||
|
#include <logging/log.h> |
||||||
|
|
||||||
|
#include <zmk/behavior.h> |
||||||
|
#include <zmk/event_manager.h> |
||||||
|
#include <zmk/events/mouse_move_state_changed.h> |
||||||
|
#include <zmk/mouse.h> |
||||||
|
|
||||||
|
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); |
||||||
|
|
||||||
|
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) |
||||||
|
|
||||||
|
static int behavior_mouse_move_init(const struct device *dev) { return 0; }; |
||||||
|
|
||||||
|
static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, |
||||||
|
struct zmk_behavior_binding_event event) { |
||||||
|
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); |
||||||
|
const struct device *dev = device_get_binding(binding->behavior_dev); |
||||||
|
const struct mouse_config *config = dev->config; |
||||||
|
return ZMK_EVENT_RAISE( |
||||||
|
zmk_mouse_move_state_changed_from_encoded(binding->param1, *config, true, event.timestamp)); |
||||||
|
} |
||||||
|
|
||||||
|
static int on_keymap_binding_released(struct zmk_behavior_binding *binding, |
||||||
|
struct zmk_behavior_binding_event event) { |
||||||
|
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); |
||||||
|
const struct device *dev = device_get_binding(binding->behavior_dev); |
||||||
|
const struct mouse_config *config = dev->config; |
||||||
|
return ZMK_EVENT_RAISE(zmk_mouse_move_state_changed_from_encoded(binding->param1, *config, |
||||||
|
false, event.timestamp)); |
||||||
|
} |
||||||
|
|
||||||
|
static const struct behavior_driver_api behavior_mouse_move_driver_api = { |
||||||
|
.binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; |
||||||
|
|
||||||
|
#define KP_INST(n) \ |
||||||
|
static struct mouse_config behavior_mouse_move_config_##n = { \
|
||||||
|
.delay_ms = DT_INST_PROP(n, delay_ms), \
|
||||||
|
.time_to_max_speed_ms = DT_INST_PROP(n, time_to_max_speed_ms), \
|
||||||
|
.acceleration_exponent = DT_INST_PROP(n, acceleration_exponent), \
|
||||||
|
}; \
|
||||||
|
DEVICE_DT_INST_DEFINE(n, behavior_mouse_move_init, device_pm_control_nop, NULL, \
|
||||||
|
&behavior_mouse_move_config_##n, APPLICATION, \
|
||||||
|
CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_mouse_move_driver_api); |
||||||
|
|
||||||
|
DT_INST_FOREACH_STATUS_OKAY(KP_INST) |
||||||
|
|
||||||
|
#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ |
@ -0,0 +1,58 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#define DT_DRV_COMPAT zmk_behavior_mouse_scroll |
||||||
|
|
||||||
|
#include <device.h> |
||||||
|
#include <drivers/behavior.h> |
||||||
|
#include <logging/log.h> |
||||||
|
|
||||||
|
#include <zmk/event_manager.h> |
||||||
|
#include <zmk/events/mouse_scroll_state_changed.h> |
||||||
|
#include <zmk/behavior.h> |
||||||
|
#include <zmk/hid.h> |
||||||
|
#include <zmk/endpoints.h> |
||||||
|
|
||||||
|
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); |
||||||
|
|
||||||
|
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) |
||||||
|
|
||||||
|
static int behavior_mouse_scroll_init(const struct device *dev) { return 0; }; |
||||||
|
|
||||||
|
static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, |
||||||
|
struct zmk_behavior_binding_event event) { |
||||||
|
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); |
||||||
|
const struct device *dev = device_get_binding(binding->behavior_dev); |
||||||
|
const struct mouse_config *config = dev->config; |
||||||
|
return ZMK_EVENT_RAISE(zmk_mouse_scroll_state_changed_from_encoded(binding->param1, *config, |
||||||
|
true, event.timestamp)); |
||||||
|
} |
||||||
|
|
||||||
|
static int on_keymap_binding_released(struct zmk_behavior_binding *binding, |
||||||
|
struct zmk_behavior_binding_event event) { |
||||||
|
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); |
||||||
|
const struct device *dev = device_get_binding(binding->behavior_dev); |
||||||
|
const struct mouse_config *config = dev->config; |
||||||
|
return ZMK_EVENT_RAISE(zmk_mouse_scroll_state_changed_from_encoded(binding->param1, *config, |
||||||
|
false, event.timestamp)); |
||||||
|
} |
||||||
|
|
||||||
|
static const struct behavior_driver_api behavior_mouse_scroll_driver_api = { |
||||||
|
.binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; |
||||||
|
|
||||||
|
#define KP_INST(n) \ |
||||||
|
static struct mouse_config behavior_mouse_scroll_config_##n = { \
|
||||||
|
.delay_ms = DT_INST_PROP(n, delay_ms), \
|
||||||
|
.time_to_max_speed_ms = DT_INST_PROP(n, time_to_max_speed_ms), \
|
||||||
|
.acceleration_exponent = DT_INST_PROP(n, acceleration_exponent), \
|
||||||
|
}; \
|
||||||
|
DEVICE_DT_INST_DEFINE(n, behavior_mouse_scroll_init, device_pm_control_nop, NULL, \
|
||||||
|
&behavior_mouse_scroll_config_##n, APPLICATION, \
|
||||||
|
CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_mouse_scroll_driver_api); |
||||||
|
|
||||||
|
DT_INST_FOREACH_STATUS_OKAY(KP_INST) |
||||||
|
|
||||||
|
#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ |
@ -0,0 +1,10 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#include <kernel.h> |
||||||
|
#include <zmk/events/mouse_button_state_changed.h> |
||||||
|
|
||||||
|
ZMK_EVENT_IMPL(zmk_mouse_button_state_changed); |
@ -0,0 +1,10 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#include <kernel.h> |
||||||
|
#include <zmk/events/mouse_move_state_changed.h> |
||||||
|
|
||||||
|
ZMK_EVENT_IMPL(zmk_mouse_move_state_changed); |
@ -0,0 +1,10 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#include <kernel.h> |
||||||
|
#include <zmk/events/mouse_scroll_state_changed.h> |
||||||
|
|
||||||
|
ZMK_EVENT_IMPL(zmk_mouse_scroll_state_changed); |
@ -0,0 +1,10 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#include <kernel.h> |
||||||
|
#include <zmk/events/mouse_tick.h> |
||||||
|
|
||||||
|
ZMK_EVENT_IMPL(zmk_mouse_tick); |
@ -0,0 +1,38 @@ |
|||||||
|
# Copyright (c) 2021 The ZMK Contributors |
||||||
|
# SPDX-License-Identifier: MIT |
||||||
|
|
||||||
|
menuconfig ZMK_MOUSE |
||||||
|
bool "Enable ZMK mouse emulation" |
||||||
|
default n |
||||||
|
|
||||||
|
config ZMK_MOUSE_TICK_DURATION |
||||||
|
int "Mouse tick duration in ms" |
||||||
|
default 8 |
||||||
|
|
||||||
|
if ZMK_MOUSE |
||||||
|
|
||||||
|
choice ZMK_MOUSE_WORK_QUEUE |
||||||
|
prompt "Work queue selection for mouse events" |
||||||
|
default ZMK_MOUSE_WORK_QUEUE_DEDICATED |
||||||
|
|
||||||
|
config ZMK_MOUSE_WORK_QUEUE_SYSTEM |
||||||
|
bool "Use default system work queue for mouse events" |
||||||
|
|
||||||
|
config ZMK_MOUSE_WORK_QUEUE_DEDICATED |
||||||
|
bool "Use dedicated work queue for mouse events" |
||||||
|
|
||||||
|
endchoice |
||||||
|
|
||||||
|
if ZMK_MOUSE_WORK_QUEUE_DEDICATED |
||||||
|
|
||||||
|
config ZMK_MOUSE_DEDICATED_THREAD_STACK_SIZE |
||||||
|
int "Stack size for dedicated mouse thread/queue" |
||||||
|
default 2048 |
||||||
|
|
||||||
|
config ZMK_MOUSE_DEDICATED_THREAD_PRIORITY |
||||||
|
int "Thread priority for dedicated mouse thread/queue" |
||||||
|
default 3 |
||||||
|
|
||||||
|
endif # ZMK_MOUSE_WORK_QUEUE_DEDICATED |
||||||
|
|
||||||
|
endif |
@ -0,0 +1,160 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#include <drivers/behavior.h> |
||||||
|
#include <logging/log.h> |
||||||
|
|
||||||
|
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); |
||||||
|
|
||||||
|
#include <zmk/event_manager.h> |
||||||
|
#include <zmk/events/mouse_button_state_changed.h> |
||||||
|
#include <zmk/events/mouse_move_state_changed.h> |
||||||
|
#include <zmk/events/mouse_scroll_state_changed.h> |
||||||
|
#include <zmk/events/mouse_tick.h> |
||||||
|
#include <zmk/hid.h> |
||||||
|
#include <zmk/endpoints.h> |
||||||
|
#include <zmk/mouse.h> |
||||||
|
|
||||||
|
static struct vector2d move_speed = {0}; |
||||||
|
static struct vector2d scroll_speed = {0}; |
||||||
|
static struct mouse_config move_config = (struct mouse_config){0}; |
||||||
|
static struct mouse_config scroll_config = (struct mouse_config){0}; |
||||||
|
static int64_t start_time = 0; |
||||||
|
|
||||||
|
bool equals(const struct mouse_config *one, const struct mouse_config *other) { |
||||||
|
return one->delay_ms == other->delay_ms && |
||||||
|
one->time_to_max_speed_ms == other->time_to_max_speed_ms && |
||||||
|
one->acceleration_exponent == other->acceleration_exponent; |
||||||
|
} |
||||||
|
|
||||||
|
static void clear_mouse_state(struct k_work *work) { |
||||||
|
move_speed = (struct vector2d){0}; |
||||||
|
scroll_speed = (struct vector2d){0}; |
||||||
|
start_time = 0; |
||||||
|
zmk_hid_mouse_movement_set(0, 0); |
||||||
|
zmk_hid_mouse_scroll_set(0, 0); |
||||||
|
LOG_DBG("Clearing state"); |
||||||
|
} |
||||||
|
|
||||||
|
K_WORK_DEFINE(mouse_clear, &clear_mouse_state); |
||||||
|
|
||||||
|
void mouse_clear_cb(struct k_timer *dummy) { |
||||||
|
k_work_submit_to_queue(zmk_mouse_work_q(), &mouse_clear); |
||||||
|
} |
||||||
|
|
||||||
|
static void mouse_tick_timer_handler(struct k_work *work) { |
||||||
|
zmk_hid_mouse_movement_set(0, 0); |
||||||
|
zmk_hid_mouse_scroll_set(0, 0); |
||||||
|
LOG_DBG("Raising mouse tick event"); |
||||||
|
ZMK_EVENT_RAISE( |
||||||
|
zmk_mouse_tick(move_speed, scroll_speed, move_config, scroll_config, &start_time)); |
||||||
|
zmk_endpoints_send_mouse_report(); |
||||||
|
} |
||||||
|
|
||||||
|
K_WORK_DEFINE(mouse_tick, &mouse_tick_timer_handler); |
||||||
|
|
||||||
|
void mouse_timer_cb(struct k_timer *dummy) { |
||||||
|
LOG_DBG("Submitting mouse work to queue"); |
||||||
|
k_work_submit_to_queue(zmk_mouse_work_q(), &mouse_tick); |
||||||
|
} |
||||||
|
|
||||||
|
K_TIMER_DEFINE(mouse_timer, mouse_timer_cb, mouse_clear_cb); |
||||||
|
|
||||||
|
static int mouse_timer_ref_count = 0; |
||||||
|
|
||||||
|
void mouse_timer_ref() { |
||||||
|
if (mouse_timer_ref_count == 0) { |
||||||
|
start_time = k_uptime_get(); |
||||||
|
k_timer_start(&mouse_timer, K_NO_WAIT, K_MSEC(CONFIG_ZMK_MOUSE_TICK_DURATION)); |
||||||
|
} |
||||||
|
mouse_timer_ref_count += 1; |
||||||
|
} |
||||||
|
|
||||||
|
void mouse_timer_unref() { |
||||||
|
if (mouse_timer_ref_count > 0) { |
||||||
|
mouse_timer_ref_count--; |
||||||
|
} |
||||||
|
if (mouse_timer_ref_count == 0) { |
||||||
|
k_timer_stop(&mouse_timer); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static void listener_mouse_move_pressed(const struct zmk_mouse_move_state_changed *ev) { |
||||||
|
move_speed.x += ev->max_speed.x; |
||||||
|
move_speed.y += ev->max_speed.y; |
||||||
|
mouse_timer_ref(); |
||||||
|
} |
||||||
|
|
||||||
|
static void listener_mouse_move_released(const struct zmk_mouse_move_state_changed *ev) { |
||||||
|
move_speed.x -= ev->max_speed.x; |
||||||
|
move_speed.y -= ev->max_speed.y; |
||||||
|
mouse_timer_unref(); |
||||||
|
} |
||||||
|
|
||||||
|
static void listener_mouse_scroll_pressed(const struct zmk_mouse_scroll_state_changed *ev) { |
||||||
|
scroll_speed.x += ev->max_speed.x; |
||||||
|
scroll_speed.y += ev->max_speed.y; |
||||||
|
mouse_timer_ref(); |
||||||
|
} |
||||||
|
|
||||||
|
static void listener_mouse_scroll_released(const struct zmk_mouse_scroll_state_changed *ev) { |
||||||
|
scroll_speed.x -= ev->max_speed.x; |
||||||
|
scroll_speed.y -= ev->max_speed.y; |
||||||
|
mouse_timer_unref(); |
||||||
|
} |
||||||
|
|
||||||
|
static void listener_mouse_button_pressed(const struct zmk_mouse_button_state_changed *ev) { |
||||||
|
LOG_DBG("buttons: 0x%02X", ev->buttons); |
||||||
|
zmk_hid_mouse_buttons_press(ev->buttons); |
||||||
|
zmk_endpoints_send_mouse_report(); |
||||||
|
} |
||||||
|
|
||||||
|
static void listener_mouse_button_released(const struct zmk_mouse_button_state_changed *ev) { |
||||||
|
LOG_DBG("buttons: 0x%02X", ev->buttons); |
||||||
|
zmk_hid_mouse_buttons_release(ev->buttons); |
||||||
|
zmk_endpoints_send_mouse_report(); |
||||||
|
} |
||||||
|
|
||||||
|
int mouse_listener(const zmk_event_t *eh) { |
||||||
|
const struct zmk_mouse_move_state_changed *mmv_ev = as_zmk_mouse_move_state_changed(eh); |
||||||
|
if (mmv_ev) { |
||||||
|
if (!equals(&move_config, &(mmv_ev->config))) |
||||||
|
move_config = mmv_ev->config; |
||||||
|
|
||||||
|
if (mmv_ev->state) { |
||||||
|
listener_mouse_move_pressed(mmv_ev); |
||||||
|
} else { |
||||||
|
listener_mouse_move_released(mmv_ev); |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
const struct zmk_mouse_scroll_state_changed *msc_ev = as_zmk_mouse_scroll_state_changed(eh); |
||||||
|
if (msc_ev) { |
||||||
|
if (!equals(&scroll_config, &(msc_ev->config))) |
||||||
|
scroll_config = msc_ev->config; |
||||||
|
if (msc_ev->state) { |
||||||
|
listener_mouse_scroll_pressed(msc_ev); |
||||||
|
} else { |
||||||
|
listener_mouse_scroll_released(msc_ev); |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
const struct zmk_mouse_button_state_changed *mbt_ev = as_zmk_mouse_button_state_changed(eh); |
||||||
|
if (mbt_ev) { |
||||||
|
if (mbt_ev->state) { |
||||||
|
listener_mouse_button_pressed(mbt_ev); |
||||||
|
} else { |
||||||
|
listener_mouse_button_released(mbt_ev); |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
ZMK_LISTENER(mouse_listener, mouse_listener); |
||||||
|
ZMK_SUBSCRIPTION(mouse_listener, zmk_mouse_button_state_changed); |
||||||
|
ZMK_SUBSCRIPTION(mouse_listener, zmk_mouse_move_state_changed); |
||||||
|
ZMK_SUBSCRIPTION(mouse_listener, zmk_mouse_scroll_state_changed); |
@ -0,0 +1,30 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#include <kernel.h> |
||||||
|
#include <zmk/mouse.h> |
||||||
|
|
||||||
|
#if IS_ENABLED(CONFIG_ZMK_MOUSE_WORK_QUEUE_DEDICATED) |
||||||
|
K_THREAD_STACK_DEFINE(mouse_work_stack_area, CONFIG_ZMK_MOUSE_DEDICATED_THREAD_STACK_SIZE); |
||||||
|
static struct k_work_q mouse_work_q; |
||||||
|
#endif |
||||||
|
|
||||||
|
struct k_work_q *zmk_mouse_work_q() { |
||||||
|
#if IS_ENABLED(CONFIG_ZMK_MOUSE_WORK_QUEUE_DEDICATED) |
||||||
|
return &mouse_work_q; |
||||||
|
#else |
||||||
|
return &k_sys_work_q; |
||||||
|
#endif |
||||||
|
} |
||||||
|
|
||||||
|
int zmk_mouse_init() { |
||||||
|
#if IS_ENABLED(CONFIG_ZMK_MOUSE_WORK_QUEUE_DEDICATED) |
||||||
|
k_work_q_start(&mouse_work_q, mouse_work_stack_area, |
||||||
|
K_THREAD_STACK_SIZEOF(mouse_work_stack_area), |
||||||
|
CONFIG_ZMK_MOUSE_DEDICATED_THREAD_PRIORITY); |
||||||
|
#endif |
||||||
|
return 0; |
||||||
|
} |
@ -0,0 +1,102 @@ |
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: MIT |
||||||
|
*/ |
||||||
|
|
||||||
|
#include <logging/log.h> |
||||||
|
|
||||||
|
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); |
||||||
|
|
||||||
|
#include <zmk/event_manager.h> |
||||||
|
#include <zmk/events/mouse_tick.h> |
||||||
|
#include <zmk/endpoints.h> |
||||||
|
#include <zmk/mouse.h> |
||||||
|
|
||||||
|
#include <sys/util.h> // CLAMP |
||||||
|
|
||||||
|
#if CONFIG_MINIMAL_LIBC |
||||||
|
static float powf(float base, float exponent) { |
||||||
|
// poor man's power implementation rounds the exponent down to the nearest integer.
|
||||||
|
float power = 1.0f; |
||||||
|
for (; exponent >= 1.0f; exponent--) { |
||||||
|
power = power * base; |
||||||
|
} |
||||||
|
return power; |
||||||
|
} |
||||||
|
#else |
||||||
|
#include <math.h> |
||||||
|
#endif |
||||||
|
|
||||||
|
struct vector2d move_remainder = {0}; |
||||||
|
struct vector2d scroll_remainder = {0}; |
||||||
|
|
||||||
|
static int64_t ms_since_start(int64_t start, int64_t now, int64_t delay) { |
||||||
|
int64_t move_duration = now - (start + delay); |
||||||
|
// start can be in the future if there's a delay
|
||||||
|
if (move_duration < 0) { |
||||||
|
move_duration = 0; |
||||||
|
} |
||||||
|
return move_duration; |
||||||
|
} |
||||||
|
|
||||||
|
static float speed(const struct mouse_config *config, float max_speed, int64_t duration_ms) { |
||||||
|
// Calculate the speed based on MouseKeysAccel
|
||||||
|
// See https://en.wikipedia.org/wiki/Mouse_keys
|
||||||
|
if (duration_ms > config->time_to_max_speed_ms || config->time_to_max_speed_ms == 0 || |
||||||
|
config->acceleration_exponent == 0) { |
||||||
|
return max_speed; |
||||||
|
} |
||||||
|
float time_fraction = (float)duration_ms / config->time_to_max_speed_ms; |
||||||
|
return max_speed * powf(time_fraction, config->acceleration_exponent); |
||||||
|
} |
||||||
|
|
||||||
|
static void track_remainder(float *move, float *remainder) { |
||||||
|
float new_move = *move + *remainder; |
||||||
|
*remainder = new_move - (int)new_move; |
||||||
|
*move = (int)new_move; |
||||||
|
} |
||||||
|
|
||||||
|
static struct vector2d update_movement(struct vector2d *remainder, |
||||||
|
const struct mouse_config *config, struct vector2d max_speed, |
||||||
|
int64_t now, int64_t *start_time) { |
||||||
|
struct vector2d move = {0}; |
||||||
|
if (max_speed.x == 0 && max_speed.y == 0) { |
||||||
|
*remainder = (struct vector2d){0}; |
||||||
|
return move; |
||||||
|
} |
||||||
|
|
||||||
|
int64_t move_duration = ms_since_start(*start_time, now, config->delay_ms); |
||||||
|
move = (struct vector2d){ |
||||||
|
.x = speed(config, max_speed.x, move_duration) * CONFIG_ZMK_MOUSE_TICK_DURATION / 1000, |
||||||
|
.y = speed(config, max_speed.y, move_duration) * CONFIG_ZMK_MOUSE_TICK_DURATION / 1000, |
||||||
|
}; |
||||||
|
|
||||||
|
track_remainder(&(move.x), &(remainder->x)); |
||||||
|
track_remainder(&(move.y), &(remainder->y)); |
||||||
|
|
||||||
|
return move; |
||||||
|
} |
||||||
|
|
||||||
|
static void mouse_tick_handler(const struct zmk_mouse_tick *tick) { |
||||||
|
struct vector2d move = update_movement(&move_remainder, &(tick->move_config), tick->max_move, |
||||||
|
tick->timestamp, tick->start_time); |
||||||
|
zmk_hid_mouse_movement_update((int16_t)CLAMP(move.x, INT16_MIN, INT16_MAX), |
||||||
|
(int16_t)CLAMP(move.y, INT16_MIN, INT16_MAX)); |
||||||
|
struct vector2d scroll = update_movement(&scroll_remainder, &(tick->scroll_config), |
||||||
|
tick->max_scroll, tick->timestamp, tick->start_time); |
||||||
|
zmk_hid_mouse_scroll_update((int8_t)CLAMP(scroll.x, INT8_MIN, INT8_MAX), |
||||||
|
(int8_t)CLAMP(scroll.y, INT8_MIN, INT8_MAX)); |
||||||
|
} |
||||||
|
|
||||||
|
int zmk_mouse_tick_listener(const zmk_event_t *eh) { |
||||||
|
const struct zmk_mouse_tick *tick = as_zmk_mouse_tick(eh); |
||||||
|
if (tick) { |
||||||
|
mouse_tick_handler(tick); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
ZMK_LISTENER(zmk_mouse_tick_listener, zmk_mouse_tick_listener); |
||||||
|
ZMK_SUBSCRIPTION(zmk_mouse_tick_listener, zmk_mouse_tick); |
@ -0,0 +1 @@ |
|||||||
|
s/.*hid_listener_keycode_//p |
@ -0,0 +1,2 @@ |
|||||||
|
pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 |
||||||
|
released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 |
@ -0,0 +1,26 @@ |
|||||||
|
#include <behaviors.dtsi> |
||||||
|
#include <dt-bindings/zmk/keys.h> |
||||||
|
#include <dt-bindings/zmk/kscan_mock.h> |
||||||
|
#include <dt-bindings/zmk/mouse.h> |
||||||
|
|
||||||
|
/ { |
||||||
|
keymap { |
||||||
|
compatible = "zmk,keymap"; |
||||||
|
label ="Default keymap"; |
||||||
|
|
||||||
|
default_layer { |
||||||
|
bindings = < |
||||||
|
&mmv MOVE_LEFT &none |
||||||
|
&none &none |
||||||
|
>; |
||||||
|
}; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
&kscan { |
||||||
|
events = < |
||||||
|
ZMK_MOCK_PRESS(0,0,100) |
||||||
|
ZMK_MOCK_RELEASE(0,0,10) |
||||||
|
>; |
||||||
|
}; |
@ -0,0 +1,110 @@ |
|||||||
|
--- |
||||||
|
title: Mouse Emulation Behaviors |
||||||
|
sidebar_label: Mouse Emulation |
||||||
|
--- |
||||||
|
|
||||||
|
## Summary |
||||||
|
|
||||||
|
Mouse emulation behaviors send mouse movements, button presses or scroll actions. |
||||||
|
|
||||||
|
Please view [`dt-bindings/zmk/mouse.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/mouse.h) for a comprehensive list of signals. |
||||||
|
|
||||||
|
## Configuration options |
||||||
|
|
||||||
|
This feature should be enabled via a config option: |
||||||
|
|
||||||
|
``` |
||||||
|
CONFIG_ZMK_MOUSE=y |
||||||
|
``` |
||||||
|
|
||||||
|
This option enables several others. |
||||||
|
|
||||||
|
### Dedicated thread processing |
||||||
|
|
||||||
|
`CONFIG_ZMK_MOUSE_WORK_QUEUE_DEDICATED` is enabled by default and separates the processing of mouse signals into a dedicated thread, significantly improving performance. |
||||||
|
|
||||||
|
### Tick rate configuration |
||||||
|
|
||||||
|
`CONFIG_ZMK_MOUSE_TICK_DURATION` sets the tick rate for mouse polling. It is set to 8 ms. by default. |
||||||
|
|
||||||
|
## Keycode Defines |
||||||
|
|
||||||
|
To make it easier to encode the HID keycode numeric values, most keymaps include |
||||||
|
the [`dt-bindings/zmk/mouse.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/mouse.h) header |
||||||
|
provided by ZMK near the top: |
||||||
|
|
||||||
|
``` |
||||||
|
#include <dt-bindings/zmk/mouse.h> |
||||||
|
``` |
||||||
|
|
||||||
|
Doing so allows using a set of defines such as `MOVE_UP`, `MOVE_DOWN`, `LCLK` and `SCROLL_UP` with these behaviors. |
||||||
|
|
||||||
|
## Mouse Button Press |
||||||
|
|
||||||
|
This behavior can press/release up to 16 mouse buttons. |
||||||
|
|
||||||
|
### Behavior Binding |
||||||
|
|
||||||
|
- Reference: `&mkp` |
||||||
|
- Parameter: A `uint16` with each bit referring to a button. |
||||||
|
|
||||||
|
Example: |
||||||
|
|
||||||
|
``` |
||||||
|
&mkp LCLK |
||||||
|
``` |
||||||
|
|
||||||
|
## Mouse Movement |
||||||
|
|
||||||
|
This behavior is used to manipulate the cursor. |
||||||
|
|
||||||
|
### Behavior Binding |
||||||
|
|
||||||
|
- Reference: `&mmv` |
||||||
|
- Parameter: A `uint32` with the first 16 bits relating to horizontal movement |
||||||
|
and the last 16 - to vertical movement. |
||||||
|
|
||||||
|
Example: |
||||||
|
|
||||||
|
``` |
||||||
|
&mmv MOVE_UP |
||||||
|
``` |
||||||
|
|
||||||
|
## Mouse Scrolling |
||||||
|
|
||||||
|
This behaviour is used to scroll, both horizontally and vertically. |
||||||
|
|
||||||
|
### Behavior Binding |
||||||
|
|
||||||
|
- Reference: `&mwh` |
||||||
|
- Parameter: A `uint16` with the first 8 bits relating to horizontal movement |
||||||
|
and the last 8 - to vertical movement. |
||||||
|
|
||||||
|
Example: |
||||||
|
|
||||||
|
``` |
||||||
|
&mwh SCROLL_UP |
||||||
|
``` |
||||||
|
|
||||||
|
## Acceleration |
||||||
|
|
||||||
|
Both mouse movement and scrolling have independently configurable acceleration profiles with three parameters: delay before movement, time to max speed and the acceleration exponent. |
||||||
|
The exponent is usually set to 0 for constant speed, 1 for uniform acceleration or 2 for uniform jerk. |
||||||
|
|
||||||
|
These profiles can be configured inside your keymap: |
||||||
|
|
||||||
|
``` |
||||||
|
&mmv { |
||||||
|
time-to-max-speed-ms = <500>; |
||||||
|
}; |
||||||
|
|
||||||
|
&mwh { |
||||||
|
acceleration-exponent=<1>; |
||||||
|
}; |
||||||
|
|
||||||
|
/ { |
||||||
|
keymap { |
||||||
|
... |
||||||
|
}; |
||||||
|
}; |
||||||
|
``` |
Loading…
Reference in new issue