]> git.mar77i.info Git - zenbook_gui/commitdiff
add keyboard
authormar77i <mar77i@protonmail.ch>
Tue, 9 Sep 2025 01:08:34 +0000 (03:08 +0200)
committermar77i <mar77i@protonmail.ch>
Tue, 9 Sep 2025 01:08:34 +0000 (03:08 +0200)
13 files changed:
keyboard.py [new symlink]
keyboard/__init__.py [new file with mode: 0644]
keyboard/key.py [new file with mode: 0644]
keyboard/keyboard.py [new file with mode: 0644]
keyboard/settings.py [new file with mode: 0644]
keyboard/sounds/Clutch.mp3 [new file with mode: 0644]
keyboard/sounds/Flicker_Sound_Effect.mp3 [new file with mode: 0644]
keyboard/touchbutton.py [new file with mode: 0644]
launch.py
ui/button.py
ui/focus.py
ui/multitouch.py
ui/root.py

diff --git a/keyboard.py b/keyboard.py
new file mode 120000 (symlink)
index 0000000..a0e149e
--- /dev/null
@@ -0,0 +1 @@
+launch.py
\ No newline at end of file
diff --git a/keyboard/__init__.py b/keyboard/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/keyboard/key.py b/keyboard/key.py
new file mode 100644 (file)
index 0000000..1e00c12
--- /dev/null
@@ -0,0 +1,278 @@
+from functools import partial
+from pathlib import Path
+
+import pygame
+from pynput.keyboard import Key, KeyCode
+
+from ui import Button
+from ui.multitouch import MultitouchHandler
+
+from .touchbutton import TouchButton
+
+KEYBOARD = (
+    ("Esc", Key.esc),
+    (None, None),
+    ("F1", Key.f1),
+    ("F2", Key.f2),
+    ("F3", Key.f3),
+    ("F4", Key.f4),
+    (None, None),
+    ("F5", Key.f5),
+    ("F6", Key.f6),
+    ("F7", Key.f7),
+    ("F8", Key.f8),
+    (None, None),
+    ("F9", Key.f9),
+    ("F10", Key.f10),
+    ("F11", Key.f11),
+    ("F12", Key.f12),
+    "\n",
+    ("§", KeyCode.from_vk(0xa7), {"x_hint": 1.5}),
+    ("1", KeyCode.from_vk(0x31)),
+    ("2", KeyCode.from_vk(0x32)),
+    ("3", KeyCode.from_vk(0x33)),
+    ("4", KeyCode.from_vk(0x34)),
+    ("5", KeyCode.from_vk(0x35)),
+    ("6", KeyCode.from_vk(0x36)),
+    ("7", KeyCode.from_vk(0x37)),
+    ("8", KeyCode.from_vk(0x38)),
+    ("9", KeyCode.from_vk(0x39)),
+    ("0", KeyCode.from_vk(0x30)),
+    ("'", KeyCode.from_vk(0x27)),
+    ("^", KeyCode.from_vk(0xfe52)),
+    ("←", Key.backspace, {"x_hint": 1.5}),
+    "\n",
+    ("Tab", Key.tab, {"x_hint": 2}),
+    ("Q", KeyCode.from_vk(0x71)),
+    ("W", KeyCode.from_vk(0x77)),
+    ("E", KeyCode.from_vk(0x65)),
+    ("R", KeyCode.from_vk(0x72)),
+    ("T", KeyCode.from_vk(0x74)),
+    ("Z", KeyCode.from_vk(0x7a)),
+    ("U", KeyCode.from_vk(0x75)),
+    ("I", KeyCode.from_vk(0x69)),
+    ("O", KeyCode.from_vk(0x6f)),
+    ("P", KeyCode.from_vk(0x70)),
+    ("ü", KeyCode.from_vk(0xfc)),
+    ("¨", KeyCode.from_vk(0xfe57)),
+    ("$", KeyCode.from_vk(0x24)),
+    "\n",
+    ("CpsLk", Key.caps_lock, {"x_hint": 2.5, "led_mask": 1}),
+    ("A", KeyCode.from_vk(0x61)),
+    ("S", KeyCode.from_vk(0x73)),
+    ("D", KeyCode.from_vk(0x64)),
+    ("F", KeyCode.from_vk(0x66)),
+    ("G", KeyCode.from_vk(0x67)),
+    ("H", KeyCode.from_vk(0x68)),
+    ("J", KeyCode.from_vk(0x6a)),
+    ("K", KeyCode.from_vk(0x6b)),
+    ("L", KeyCode.from_vk(0x6c)),
+    ("ö", KeyCode.from_vk(0xf6)),
+    ("ä", KeyCode.from_vk(0xe4)),
+    ("↵", Key.enter, {"x_hint": 1.5}),
+    "\n",
+    ("↑", Key.shift, {"x_hint": 2}),
+    ("<", KeyCode.from_vk(0x3c)),
+    ("Y", KeyCode.from_vk(0x79)),
+    ("X", KeyCode.from_vk(0x78)),
+    ("C", KeyCode.from_vk(0x63)),
+    ("V", KeyCode.from_vk(0x76)),
+    ("B", KeyCode.from_vk(0x62)),
+    ("N", KeyCode.from_vk(0x6e)),
+    ("M", KeyCode.from_vk(0x6d)),
+    (",", KeyCode.from_vk(0x2c)),
+    (".", KeyCode.from_vk(0x2e)),
+    ("-", KeyCode.from_vk(0x2d)),
+    ("↑", Key.shift, {"x_hint": 2}),
+    "\n",
+    ("Ctrl", Key.ctrl_l, {"x_hint": 2}),
+    ("⊞", Key.cmd),
+    ("Alt", Key.alt_l),
+    ("␣", Key.space, {"x_hint": 7}),
+    ("⚙", "Settings"),
+    ("AltGr", KeyCode.from_vk(0xfe03)),
+    ("Ctrl", Key.ctrl_r, {"x_hint": 2}),
+)
+
+CURSOR_KEYS = (
+    ("INS", Key.insert),
+    ("HOME", Key.home),
+    ("PG UP", Key.page_up),
+    "\n",
+    ("DEL", Key.delete),
+    ("END", Key.end),
+    ("PG DN", Key.page_down),
+    "\n",
+    "\n",
+    (None, None),
+    ("↑", Key.up),
+    (None, None),
+    "\n",
+    ("←", Key.left),
+    ("↓", Key.down),
+    ("→", Key.right),
+)
+
+KEYPAD_KEYS = (
+    ("NUM", Key.num_lock, {"led_mask": 2}),
+    ("/", KeyCode.from_vk(0xffaf)),
+    ("*", KeyCode.from_vk(0xffaa)),
+    ("-", KeyCode.from_vk(0xffad)),
+    "\n",
+    ("7", KeyCode.from_vk(0xffb7)),
+    ("8", KeyCode.from_vk(0xffb8)),
+    ("9", KeyCode.from_vk(0xffb9)),
+    ("+", KeyCode.from_vk(0xffab), {"y_hint": 2}),
+    "\n",
+    ("4", KeyCode.from_vk(0xffb4)),
+    ("5", KeyCode.from_vk(0xffb5)),
+    ("6", KeyCode.from_vk(0xffb6)),
+    "\n",
+    ("1", KeyCode.from_vk(0xffb1)),
+    ("2", KeyCode.from_vk(0xffb2)),
+    ("3", KeyCode.from_vk(0xffb3)),
+    ("Enter", KeyCode.from_vk(0xff8d), {"y_hint": 2}),
+    "\n",
+    ("0", KeyCode.from_vk(0xffb0), {"x_hint": 2}),
+    (".", KeyCode.from_vk(0xffae)),
+)
+
+
+class KeyButton(Button):
+    def __init__(self, *args, **kwargs):
+        self.data = kwargs.pop("data", None)
+        self.led_mask = kwargs.pop("led_mask", None)
+        super().__init__(*args, **kwargs)
+        self.touch_key = None
+        # sounds from https://freesoundsite.com/sound-collections/click-sound-effects/
+        self.sounds = (
+            pygame.mixer.Sound(Path(__file__).parent / "sounds" / "Flicker_Sound_Effect.mp3"),
+            pygame.mixer.Sound(Path(__file__).parent / "sounds" / "Clutch.mp3"),
+        )
+        for sound in self.sounds:
+            sound.set_volume(0.5)
+
+    def press(self, touch_key=None):
+        if self.pushed:
+            return
+        if not self.root.muted:
+            self.sounds[0].play()
+        self.touch_key = touch_key
+        self.pushed = True
+        self.dirty = True
+        self.callback(self.data)
+
+    def release(self, touch_key=None):
+        if not self.pushed or touch_key != self.touch_key:
+            return
+        if not self.root.muted:
+            self.sounds[1].play()
+        self.touch_key = None
+        self.pushed = False
+        self.dirty = True
+        self.callback(self.data, True)
+
+    def handle_mousebuttondown(self, event):
+        if event.button == 1 and self.rect.collidepoint(event.pos):
+            self.press()
+
+    def handle_mousemotion(self, event):
+        if event.buttons[0] and not self.rect.collidepoint(event.pos):
+            self.release()
+
+    def handle_mousebuttonup(self, event):
+        if event.button == 1 and self.rect.collidepoint(event.pos):
+            self.release()
+
+    def handle_fingerdown(self, event):
+        if self.rect.collidepoint(
+            MultitouchHandler.map_coords((event.x, event.y), self.surf.get_size())
+        ):
+            self.press((event.touch_id, event.finger_id))
+
+    def handle_fingermotion(self, event):
+        if not self.rect.collidepoint(
+            MultitouchHandler.map_coords((event.x, event.y), self.surf.get_size())
+        ):
+            self.release((event.touch_id, event.finger_id))
+
+    def handle_fingerup(self, event):
+        self.release((event.touch_id, event.finger_id))
+
+    def draw(self):
+        if self.led_mask:
+            self.highlight = self.root.led_mask & self.led_mask
+        super().draw()
+
+
+def apply_y_spans(x_sums, y_spans):
+    for i, (width, y_remaining) in enumerate(y_spans):
+        if y_remaining <= 0:
+            continue
+        x_sums[-1] += width
+        y_spans[i][1] = y_remaining - 1
+
+
+def get_key_size(size, x_sums_iter, x_sums_len):
+    x_sum = next(x_sums_iter)
+    if x_sum == 0:
+        return (0, size[1] / x_sums_len)
+    return (size[0] / x_sum, size[1] / x_sums_len)
+
+
+def get_keyboard_keys(rect, parent, callback, keys):
+    result = []
+    x_sums = [0]
+    y_spans = []
+    for key in keys:
+        if key == "\n":
+            x_sums.append(0)
+            apply_y_spans(x_sums, y_spans)
+        elif len(key) > 2:
+            width = key[2].get("x_hint", 1)
+            x_sums[-1] += width
+            y_hint = key[2].get("y_hint", 1)
+            if y_hint > 1:
+                y_spans.append([width, y_hint - 1])
+        else:
+            x_sums[-1] += 1
+    apply_y_spans(x_sums, y_spans)
+    assert all(item[1] <= 0 for item in y_spans)
+    x, y = rect.topleft
+    x_sums_iter = iter(x_sums)
+    x_sums_len = len(x_sums)
+    key_size = get_key_size(rect.size, x_sums_iter, x_sums_len)
+    for key in keys:
+        if key == "\n":
+            x, y = rect.left, y + key_size[1]
+            key_size = get_key_size(rect.size, x_sums_iter, x_sums_len)
+            continue
+        key_width, key_height = key_size
+        if key[1] == "Settings":
+            result.append(
+                TouchButton(
+                    parent,
+                    pygame.Rect((x + 8, y + 8), (key_width - 16, key_height - 16)),
+                    key[0],
+                    partial(callback, key[1]),
+                )
+            )
+        elif key != (None, None):
+            if len(key) > 2:
+                key_width *= key[2].get("x_hint", 1)
+                key_height *= key[2].get("y_hint", 1)
+                led_mask = key[2].get("led_mask")
+            else:
+                led_mask = None
+            result.append(
+                KeyButton(
+                    parent,
+                    pygame.Rect((x + 8, y + 8), (key_width - 16, key_height - 16)),
+                    key[0],
+                    callback,
+                    data=key[1],
+                    led_mask=led_mask
+                )
+            )
+        x += key_width
+    return result
diff --git a/keyboard/keyboard.py b/keyboard/keyboard.py
new file mode 100644 (file)
index 0000000..74af025
--- /dev/null
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+
+from pynput.keyboard import Controller as KeyboardController, Key, KeyCode
+from pynput._util.xorg_keysyms import KEYPAD_KEYS
+
+from launch import pygame
+from multitouch import MultitouchHandler
+from ui import BaseRoot, FPSWidget
+
+from .key import CURSOR_KEYS, KEYBOARD, KEYPAD_KEYS, get_keyboard_keys
+from .settings import Settings
+
+# add a launcher symbol
+
+
+class Root(BaseRoot):
+    BACKGROUND_COLOR = "black"
+
+    def __init__(self):
+        super().__init__(
+            pygame.display.set_mode((0, 0), pygame.NOFRAME, display=1),
+            pygame.font.SysFont("DejaVu Sans", size=64),
+        )
+        size = self.surf.get_size()
+        widget_size = (size[0] // 3, size[1] // 2)
+        get_keyboard_keys(
+            pygame.Rect((0, 0), (size[0], widget_size[1])), self, self.key_cb, KEYBOARD
+        )
+        get_keyboard_keys(
+            pygame.Rect((0, widget_size[1]), widget_size),
+            self,
+            self.key_cb,
+            CURSOR_KEYS,
+        )
+        get_keyboard_keys(
+            pygame.Rect(widget_size, widget_size), self, self.key_cb, KEYPAD_KEYS
+        )
+        self.pushed = set()
+        self.mt = MultitouchHandler(
+            MultitouchHandler.find_device(name="ELAN9009:00 04F3:425A")
+        )
+        self.keyboard = KeyboardController()
+        self.display = self.keyboard._display
+        self.led_mask = self.display.get_keyboard_control().led_mask
+        self.muted = False
+        FPSWidget(self)
+        self.settings = Settings(
+            self,
+            pygame.Rect((size[0] // 4, size[1] // 4), (size[0] // 2, size[1] // 2)),
+        )
+
+    def key_cb(self, key, release=False):
+        if key == "Settings":
+            self.settings.activate()
+        if isinstance(key, (Key, KeyCode)):
+            (self.keyboard.press, self.keyboard.release)[release](key)
+            if release and key in self.pushed:
+                self.pushed.remove(key)
+            elif not release:
+                self.pushed.add(key)
+
+    def handle_events(self):
+        self.mt.handle_events()
+        super().handle_events()
+
+    def update(self):
+        super().update()
+        led_mask = self.keyboard._display.get_keyboard_control().led_mask
+        if led_mask != self.led_mask:
+            self.led_mask = led_mask
+            self.dirty = True
+
+    def run(self):
+        try:
+            with self.mt.device.grab_context():
+                super().run()
+        finally:
+            for key in self.pushed:
+                self.keyboard.release(key)
diff --git a/keyboard/settings.py b/keyboard/settings.py
new file mode 100644 (file)
index 0000000..f88f23e
--- /dev/null
@@ -0,0 +1,56 @@
+import os
+import sys
+from pathlib import Path
+
+import pygame
+
+from ui import Modal, Rect
+
+from .touchbutton import TouchButton
+
+# what settings do we need?
+# - configure widgets
+#   - keyboard above, two widgets below
+#   - keyboard above, three widgets below
+#   - keyboard below, two widgets above
+#   - keyboard below, three widgets above
+#   - three available widgets: touchpad mouse, number keypad, cursor and navkeys
+
+
+class Settings(Modal):
+    def __init__(self, parent, rect):
+        super().__init__(parent)
+        self.rect = rect
+        Rect(self, self.rect, "black", "gray")
+        width = rect.width * 2 / 3
+        height = rect.height / 7
+        left = rect.left + (rect.width - width) / 2 + 8
+        y = rect.top + height + 8
+        rect_size = (width - 16, height - 16)
+        self.buttons = []
+        for args in (
+            ("Mute", self.mute, self.root.muted),
+            ("Layout...", self.layout),
+            ("Restart", self.restart),
+            ("Exit", self.root.handle_quit),
+            ("Back", self.deactivate),
+        ):
+            self.buttons.append(
+                TouchButton(self, pygame.Rect((left, y), rect_size), *args)
+            )
+            y += height
+
+    def mute(self):
+        self.root.muted ^= True
+        self.buttons[0].highlight = self.root.muted
+
+    def layout(self):
+        print("stub layout")
+
+    def restart(self):
+        executable = Path(sys.executable).name
+        os.execl(
+            sys.executable,
+            executable,
+            *sys.orig_argv[sys.orig_argv[0] == executable:],
+        )
diff --git a/keyboard/sounds/Clutch.mp3 b/keyboard/sounds/Clutch.mp3
new file mode 100644 (file)
index 0000000..2a4bcf1
Binary files /dev/null and b/keyboard/sounds/Clutch.mp3 differ
diff --git a/keyboard/sounds/Flicker_Sound_Effect.mp3 b/keyboard/sounds/Flicker_Sound_Effect.mp3
new file mode 100644 (file)
index 0000000..db8df4f
Binary files /dev/null and b/keyboard/sounds/Flicker_Sound_Effect.mp3 differ
diff --git a/keyboard/touchbutton.py b/keyboard/touchbutton.py
new file mode 100644 (file)
index 0000000..1289e34
--- /dev/null
@@ -0,0 +1,41 @@
+from ui import Button
+from ui.multitouch import MultitouchHandler
+
+
+class TouchButton(Button):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.touch_key = None
+
+    def handle_fingerdown(self, event):
+        if not self.pushed and self.rect.collidepoint(
+            MultitouchHandler.map_coords((event.x, event.y), self.surf.get_size())
+        ):
+            self.touch_key = (event.touch_id, event.finger_id)
+            self.pushed = True
+            self.dirty = True
+
+    def handle_fingermotion(self, event):
+        if not self.pushed or self.touch_key is None:
+            return
+        if (
+            (event.touch_id, event.finger_id) == self.touch_key
+            and not self.rect.collidepoint(
+                MultitouchHandler.map_coords((event.x, event.y), self.surf.get_size())
+            )
+        ):
+            self.touch_key = None
+            self.pushed = False
+            self.dirty = True
+
+    def handle_fingerup(self, event):
+        if not self.pushed or self.touch_key is None:
+            return
+        if (event.touch_id, event.finger_id) == self.touch_key:
+            if self.rect.collidepoint(
+                MultitouchHandler.map_coords((event.x, event.y), self.surf.get_size())
+            ):
+                self.callback()
+            self.touch_key = None
+            self.pushed = False
+            self.dirty = True
index 7156e309fa41e2985142bf92c5e319b37fae2ea4..95d345de129ce5fbf34786c7696c7fcdd2668a08 100755 (executable)
--- a/launch.py
+++ b/launch.py
@@ -127,7 +127,19 @@ def setup_venv():
         if not has_pygame or all(
             p.stat().st_mtime < yesterday for p in walk_outer(venv_dir)
         ):
-            run_cmd((executable, "-m", "pip", "install", "-qU", "pip", "pygame"))
+            run_cmd(
+                (
+                    executable,
+                    "-m",
+                    "pip",
+                    "install",
+                    "-qU",
+                    "pip",
+                    "pygame",
+                    "pynput",
+                    "evdev",
+                )
+            )
             venv_dir.touch(0o755, True)
     elif not has_pygame:
         raise ConnectionError("Internet needed to install requirement: pygame")
index 15cf575d179971b3dc3b21cdd476b600f7e9e82b..1758f8161c7ec0defc0b67d8d3c77b19b0106f9d 100644 (file)
@@ -33,15 +33,16 @@ class Button(Child):
             self.pushed = True
             self.dirty = True
 
-    def handle_mousebuttonup(self, ev):
-        if ev.button == 1 and self.pushed and self.rect.collidepoint(ev.pos):
-            self.pushed = False
-            self.callback()
-            self.dirty = True
-
     def handle_mousemotion(self, ev):
         if not self.pushed:
             return
         if ev.buttons[0] and not self.rect.collidepoint(ev.pos):
             self.pushed = False
             self.dirty = True
+
+    def handle_mousebuttonup(self, ev):
+        if ev.button == 1 and self.pushed:
+            if self.rect.collidepoint(ev.pos):
+                self.callback()
+            self.pushed = False
+            self.dirty = True
index 1432a38e207e0ae81e542845535fbf0b9a3f44e1..b372529c422470f1102ad6223c75340d85239eda 100644 (file)
@@ -1,8 +1,8 @@
-from .root import BaseRoot
+import ui
 
 
 class Focusable:
-    root: BaseRoot
+    root: "ui.root.BaseRoot"
     dirty: bool
 
     @property
index 5336cc960a74a19d80eac2e234f931cf4a20da0d..c24c91db9e8e262c2e0724526908e88a558318fa 100755 (executable)
@@ -1,9 +1,18 @@
+import pygame
 from evdev import InputDevice, ecodes, list_devices
 
 
 class MultitouchHandler:
+    STRINGY_FIELDS = False
+
+    @staticmethod
+    def map_coords(event_pos, size):
+        return (event_pos[0] * size[0], event_pos[1] * size[1])
+
     @staticmethod
     def find_device(**kwargs):
+        if "path" in kwargs:
+            return InputDevice(kwargs["path"])
         for path in list_devices():
             device = InputDevice(path)
             for k, v in kwargs.items():
@@ -13,74 +22,144 @@ class MultitouchHandler:
                 return device
         return None
 
-    @staticmethod
-    def multitouch_ranges(device):
-        return {
-            v: (info.min, info.max)
-            for v, info in (
-                (v, device.absinfo(v))
-                for v in (
-                    ecodes.ABS_X,
-                    ecodes.ABS_Y,
-                    ecodes.ABS_MT_POSITION_X,
-                    ecodes.ABS_MT_POSITION_Y,
-                )
-            )
+    def __init__(self, device):
+        self.device = device
+        self.touch_id = int(self.device.path[self.device.path.find("event") + 5:])
+        x_info = self.device.absinfo(ecodes.ABS_MT_POSITION_X)
+        y_info = self.device.absinfo(ecodes.ABS_MT_POSITION_Y)
+        self.info = {
+            ecodes.ABS_MT_POSITION_X: (x_info.min, x_info.max),
+            ecodes.ABS_MT_POSITION_Y: (y_info.min, y_info.max),
         }
-
-    def __init__(self, **kwargs):
-        self.device = self.find_device(**kwargs)
-        self.info = self.multitouch_ranges(self.device)
-        self.context_app = kwargs.get("context_app")
-        self.slots = [{}]
+        self._fingers = [{}]
+        self.fingers = []
+        self.dropped = False
         self.slot = 0
+        self.event_handlers = {
+            ecodes.EV_SYN: self.handle_syn,
+            ecodes.EV_ABS: self.handle_abs,
+            ecodes.EV_MSC: self.handle_msc,
+            ecodes.EV_KEY: self.handle_key,
+        }
+        self.tracking_id_lookup = self.get_lookup(ecodes.ABS, ecodes.ABS_MT_TRACKING_ID)
+        self.x_lookup = self.get_lookup(ecodes.ABS, ecodes.ABS_MT_POSITION_X)
+        self.y_lookup = self.get_lookup(ecodes.ABS, ecodes.ABS_MT_POSITION_Y)
+
+    def current_finger(self):
+        while self.slot >= len(self._fingers):
+            self._fingers.append({})
+        return self._fingers[self.slot]
+
+    def _clear_fingers(self):
+        for finger in self._fingers:
+            finger.clear()
+
+    @classmethod
+    def get_lookup(cls, mapping, code):
+        if cls.STRINGY_FIELDS:
+            return mapping[code]
+        return code
+
+    def push_event(self, event_type, finger_new, finger_old):
+        x_info = self.info[ecodes.ABS_MT_POSITION_X]
+        y_info = self.info[ecodes.ABS_MT_POSITION_Y]
+        x = (finger_new.get(self.x_lookup, x_info[1]) - x_info[0]) / (x_info[1] - x_info[0])
+        y = (finger_new.get(self.y_lookup, x_info[1]) - y_info[0]) / (y_info[1] - y_info[0])
+        pygame.event.post(
+            pygame.event.Event(
+                event_type,
+                {
+                    "finger_id": finger_new[self.tracking_id_lookup],
+                    "touch_id": self.touch_id,
+                    "x": x,
+                    "y": y,
+                    **(
+                        {
+                            "dx": x - (
+                                finger_old.get(self.x_lookup, x_info[1]) - x_info[0]
+                            ) / x_info[1],
+                            "dy": y - (
+                                finger_old.get(self.y_lookup, y_info[1]) - y_info[0]
+                            ) / y_info[1],
+                        } if finger_old is not None else {"dx": 0.0, "dy": 0.0}
+                    ),
+                    "evdev_event": finger_new.copy(),
+                    **({"dropped": True} if self.dropped else {}),
+                }
+            )
+        )
 
-    def current_slot(self):
-        while self.slot >= len(self.slots):
-            self.slots.append({})
-        return self.slots[self.slot]
+    @staticmethod
+    def cmp_fingers(finger_new, finger_old):
+        for key in (*finger_new, *finger_old):
+            if key == ecodes.MSC_TIMESTAMP:
+                continue
+            if finger_new.get(key) != finger_old.get(key):
+                return True
+        return False
+
+    def sync(self):
+        num_fingers = len(self._fingers)
+        while len(self.fingers) < num_fingers:
+            self.fingers.append({})
+        for finger_new, finger_old in zip(self._fingers, self.fingers):
+            old_finger_id = finger_old.get(self.tracking_id_lookup)
+            new_finger_id = finger_new.get(self.tracking_id_lookup)
+            if old_finger_id is not None:
+                if old_finger_id == new_finger_id:
+                    if self.cmp_fingers(finger_new, finger_old):
+                        self.push_event(pygame.FINGERMOTION, finger_new, finger_old)
+                else:
+                    self.push_event(pygame.FINGERUP, finger_old, None)
+            if new_finger_id is not None and (
+                old_finger_id is None or old_finger_id != new_finger_id
+            ):
+                self.push_event(pygame.FINGERDOWN, finger_new, None)
+            finger_old.clear()
+            finger_old.update(finger_new)
+
+    def handle_syn(self, event):
+        if event.code == ecodes.SYN_DROPPED:
+            self._clear_fingers()
+            self.dropped = True
+        elif event.code == ecodes.SYN_REPORT:
+            self.sync()
+            self.dropped = False
+
+    def handle_abs(self, event):
+        if event.code == ecodes.ABS_MT_SLOT:
+            self.slot = event.value
+        elif event.code == ecodes.ABS_MT_TRACKING_ID and event.value ==  -1:
+            self.current_finger().clear()
+            return
+        self.current_finger()[self.get_lookup(ecodes.ABS, event.code)] = event.value
+
+    def handle_msc(self, event):
+        self.current_finger()[self.get_lookup(ecodes.MSC, event.code)] = event.value
 
-    def handle_event(self, event):
+    def handle_key(self, event):
+        if event.code == ecodes.BTN_TOUCH and event.value == 0:
+            self._clear_fingers()
+
+    @staticmethod
+    def log_event(event):
         if event.type == ecodes.EV_SYN:
-            current_slot = self.current_slot()
-            if current_slot.get(ecodes.ABS_MT_TRACKING_ID) == -1:
-                current_slot.clear()
+            code = ecodes.SYN[event.code]
         elif event.type == ecodes.EV_ABS:
-            if event.code == ecodes.ABS_MT_SLOT:
-                self.slot = event.value
-            else:
-                self.current_slot()[event.code] = event.value
+            code = ecodes.ABS[event.code]
         elif event.type == ecodes.EV_MSC:
-            self.current_slot()[event.code] = event.value
-        elif event.type == ecodes.EV_KEY:
-            if event.code == ecodes.BTN_TOUCH:
-                if event.value == 0:
-                    for s in self.slots:
-                        s.clear()
+            code = ecodes.MSC[event.code]
+        elif event.type == ecodes.EV_KEY and event.code in {
+            *ecodes.KEY, *ecodes.BTN
+        }:
+            code = ecodes.KEY.get(
+                event.code, ecodes.BTN[event.code]
+            )
+        else:
+            code = f"{ecodes.EV[event.type]}[{event.code}]"
+        print(ecodes.EV[event.type], code, event.value)
 
     def handle_events(self):
         while (event := self.device.read_one()) is not None:
-            self.handle_event(event)
-
-    def map_coords(self, rect):
-        for slot in self.slots:
-            if ecodes.ABS_MT_POSITION_X in slot and ecodes.ABS_MT_POSITION_Y in slot:
-                infos = (
-                    self.info[ecodes.ABS_MT_POSITION_X],
-                    self.info[ecodes.ABS_MT_POSITION_Y],
-                )
-                pos = (slot[ecodes.ABS_MT_POSITION_X], slot[ecodes.ABS_MT_POSITION_Y])
-            elif ecodes.ABS_X in slot and ecodes.ABS_Y in slot:
-                infos = (self.info[ecodes.ABS_X], self.info[ecodes.ABS_Y])
-                pos = (slot[ecodes.ABS_X], slot[ecodes.ABS_Y])
-            else:
-                continue
-            yield tuple(
-                offset + (value - info[0]) * scale / (info[1] - info[0])
-                for value, info, offset, scale in zip(
-                    pos,
-                    infos,
-                    rect.topleft,
-                    rect.size,
-                )
-            ), slot
+            # self.log_event(event)
+            self.event_handlers.get(event.type, lambda e: None)(event)
index 37264825dcb73b5518bf4823cad8ebd9ffeff50e..2d63201a114799dc59adb16526c7424ec81d7a56 100644 (file)
@@ -1,9 +1,10 @@
 import pygame
 
+from .focus import Focusable
 from .parent import Parent
 
 
-class BaseRoot(Parent):
+class BaseRoot(Focusable, Parent):
     BACKGROUND_COLOR: pygame.Color
     ACTIVATE_EVENTS = (
         pygame.WINDOWENTER,
@@ -29,9 +30,10 @@ class BaseRoot(Parent):
 
     key_methods = {frozenset(): {pygame.K_ESCAPE: handle_quit}}
 
-    @property
-    def is_focused(self):
-        return self.focus_stack[-1] is self
+    def handle_events(self):
+        for ev in pygame.event.get():
+            self.stop_event = False
+            self.handle_event(ev)
 
     def handle_event(self, ev):
         if ev.type in self.ACTIVATE_EVENTS:
@@ -53,12 +55,12 @@ class BaseRoot(Parent):
 
     def run(self):
         while True:
-            for ev in pygame.event.get():
-                self.stop_event = False
-                self.handle_event(ev)
-                if not self.running:
-                    return
+            self.handle_events()
+            if not self.running:
+                return
             self.update()
+            if not self.running:
+                return
             if self.dirty:
                 self.draw()
                 pygame.display.update()