From 681c1ad03313e5cac1b08e030d7f0f84700868be Mon Sep 17 00:00:00 2001
From: Amine Hassane <sporif@posteo.net>
Date: Sun, 21 Apr 2024 14:48:26 +0100
Subject: [PATCH] wayland: implement wl_touch

Co-authored-by: Julian Orth <ju.orth@gmail.com>
---
 docs/features.md                        |   9 +-
 jay-config/src/_private/client.rs       |   4 +
 jay-config/src/_private/ipc.rs          |   4 +
 jay-config/src/input.rs                 |   7 +
 release-notes.md                        |   1 +
 src/backend.rs                          |  27 ++++
 src/backends/metal.rs                   |  25 ++++
 src/backends/metal/input.rs             |  46 +++++++
 src/cli/input.rs                        |  39 ++++++
 src/cli/seat_test.rs                    |  51 +++++++-
 src/config/handler.rs                   |  13 ++
 src/ifs/jay_compositor.rs               |   4 +-
 src/ifs/jay_input.rs                    |  34 ++++-
 src/ifs/jay_seat_events.rs              |  40 ++++++
 src/ifs/wl_seat.rs                      |  41 +++++-
 src/ifs/wl_seat/event_handling.rs       | 166 ++++++++++++++++++++++-
 src/ifs/wl_seat/touch_owner.rs          | 167 ++++++++++++++++++++++++
 src/ifs/wl_seat/wl_touch.rs             |  86 ++++++++++--
 src/ifs/wl_surface.rs                   |  34 +++++
 src/libinput/device.rs                  |  23 +++-
 src/libinput/event.rs                   |  68 +++++++---
 src/libinput/sys.rs                     |  25 ++++
 src/state.rs                            |   1 +
 src/tasks/input_device.rs               |   3 +-
 src/tools/tool_client.rs                |   2 +-
 src/tree.rs                             |  44 +++++++
 toml-config/src/config.rs               |   1 +
 toml-config/src/config/parsers/input.rs |  61 +++++++++
 toml-config/src/lib.rs                  |   3 +
 toml-spec/spec/spec.generated.json      |  12 ++
 toml-spec/spec/spec.generated.md        |  15 +++
 toml-spec/spec/spec.yaml                |  18 +++
 wire/jay_input.txt                      |  19 +++
 wire/jay_seat_events.txt                |  28 ++++
 wire/wl_touch.txt                       |   2 +-
 35 files changed, 1071 insertions(+), 52 deletions(-)
 create mode 100644 src/ifs/wl_seat/touch_owner.rs

diff --git a/docs/features.md b/docs/features.md
index 12605549..4bca24ce 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -141,7 +141,7 @@ Jay supports the following wayland protocols:
 | ext_session_lock_manager_v1             | 1               | Yes           |
 | ext_transient_seat_manager_v1           | 1[^ts_rejected] | Yes           |
 | org_kde_kwin_server_decoration_manager  | 1               |               |
-| wl_compositor                           | 6[^no_touch]    |               |
+| wl_compositor                           | 6               |               |
 | wl_data_device_manager                  | 3               |               |
 | wl_drm                                  | 2               |               |
 | wl_output                               | 4               |               |
@@ -179,12 +179,5 @@ Jay supports the following wayland protocols:
 | zxdg_decoration_manager_v1              | 1               |               |
 | zxdg_output_manager_v1                  | 3               |               |
 
-[^no_touch]: Touch input is not supported.
 [^lsaccess]: Sandboxes can restrict access to this protocol.
 [^ts_rejected]: Seat creation is always rejected.
-
-## Missing Features
-
-The following features are currently not supported but might get implemented in the future:
-
-- Touch support.
diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs
index 8b7f9115..cc2bf7f3 100644
--- a/jay-config/src/_private/client.rs
+++ b/jay-config/src/_private/client.rs
@@ -897,6 +897,10 @@ impl Client {
         self.send(&ClientMessage::SetTransformMatrix { device, matrix })
     }
 
+    pub fn set_calibration_matrix(&self, device: InputDevice, matrix: [[f32; 3]; 2]) {
+        self.send(&ClientMessage::SetCalibrationMatrix { device, matrix })
+    }
+
     pub fn set_px_per_wheel_scroll(&self, device: InputDevice, px: f64) {
         self.send(&ClientMessage::SetPxPerWheelScroll { device, px })
     }
diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs
index 573c83d6..e53d2350 100644
--- a/jay-config/src/_private/ipc.rs
+++ b/jay-config/src/_private/ipc.rs
@@ -502,6 +502,10 @@ pub enum ClientMessage<'a> {
         connector: Option<Connector>,
         mode: TearingMode,
     },
+    SetCalibrationMatrix {
+        device: InputDevice,
+        matrix: [[f32; 3]; 2],
+    },
 }
 
 #[derive(Serialize, Deserialize, Debug)]
diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs
index e472640c..349a868a 100644
--- a/jay-config/src/input.rs
+++ b/jay-config/src/input.rs
@@ -80,6 +80,13 @@ impl InputDevice {
         get!().set_transform_matrix(self, matrix);
     }
 
+    /// Sets the calibration matrix of the device.
+    ///
+    /// This corresponds to the libinput setting of the same name.
+    pub fn set_calibration_matrix(self, matrix: [[f32; 3]; 2]) {
+        get!().set_calibration_matrix(self, matrix);
+    }
+
     /// Returns the name of the device.
     pub fn name(self) -> String {
         get!(String::new()).device_name(self)
diff --git a/release-notes.md b/release-notes.md
index 0ec87cae..e7e1abd0 100644
--- a/release-notes.md
+++ b/release-notes.md
@@ -3,6 +3,7 @@
 - Add fine-grained damage tracking.
 - Add support for adaptive sync.
 - Add support for tearing.
+- Add support for touch input.
 
 # 1.4.0 (2024-07-07)
 
diff --git a/src/backend.rs b/src/backend.rs
index f3e40ff9..1ede68ed 100644
--- a/src/backend.rs
+++ b/src/backend.rs
@@ -167,6 +167,12 @@ pub trait InputDevice {
         None
     }
     fn set_transform_matrix(&self, matrix: TransformMatrix);
+    fn calibration_matrix(&self) -> Option<[[f32; 3]; 2]> {
+        None
+    }
+    fn set_calibration_matrix(&self, m: [[f32; 3]; 2]) {
+        let _ = m;
+    }
     fn name(&self) -> Rc<String>;
     fn dev_t(&self) -> Option<c::dev_t> {
         None
@@ -392,6 +398,27 @@ pub enum InputEvent {
         source: Option<TabletStripEventSource>,
         position: Option<f64>,
     },
+    TouchDown {
+        time_usec: u64,
+        id: i32,
+        x_normed: Fixed,
+        y_normed: Fixed,
+    },
+    TouchUp {
+        time_usec: u64,
+        id: i32,
+    },
+    TouchMotion {
+        time_usec: u64,
+        id: i32,
+        x_normed: Fixed,
+        y_normed: Fixed,
+    },
+    TouchCancel {
+        time_usec: u64,
+        id: i32,
+    },
+    TouchFrame,
 }
 
 pub enum DrmEvent {
diff --git a/src/backends/metal.rs b/src/backends/metal.rs
index 880b18da..c40cd371 100644
--- a/src/backends/metal.rs
+++ b/src/backends/metal.rs
@@ -376,6 +376,7 @@ struct InputDeviceProperties {
     drag_enabled: Cell<Option<bool>>,
     drag_lock_enabled: Cell<Option<bool>>,
     natural_scrolling_enabled: Cell<Option<bool>>,
+    calibration_matrix: Cell<Option<[[f32; 3]; 2]>>,
 }
 
 #[derive(Clone)]
@@ -436,6 +437,9 @@ impl MetalInputDevice {
         if let Some(enabled) = self.desired.natural_scrolling_enabled.get() {
             self.set_natural_scrolling_enabled(enabled);
         }
+        if let Some(lh) = self.desired.calibration_matrix.get() {
+            self.set_calibration_matrix(lh);
+        }
         self.fetch_effective();
     }
 
@@ -465,6 +469,11 @@ impl MetalInputDevice {
                 .natural_scrolling_enabled
                 .set(Some(device.natural_scrolling_enabled()));
         }
+        if device.has_calibration_matrix() {
+            self.effective
+                .calibration_matrix
+                .set(Some(device.get_calibration_matrix()));
+        }
     }
 
     fn pre_pause(&self) {
@@ -721,6 +730,22 @@ impl InputDevice for MetalInputDevice {
             groups,
         }))
     }
+
+    fn calibration_matrix(&self) -> Option<[[f32; 3]; 2]> {
+        self.effective.calibration_matrix.get()
+    }
+
+    fn set_calibration_matrix(&self, m: [[f32; 3]; 2]) {
+        self.desired.calibration_matrix.set(Some(m));
+        if let Some(dev) = self.inputdev.get() {
+            if dev.device().has_calibration_matrix() {
+                dev.device().set_calibration_matrix(m);
+                self.effective
+                    .calibration_matrix
+                    .set(Some(dev.device().get_calibration_matrix()));
+            }
+        }
+    }
 }
 
 impl MetalInputDevice {
diff --git a/src/backends/metal/input.rs b/src/backends/metal/input.rs
index 18911d2e..8f638a79 100644
--- a/src/backends/metal/input.rs
+++ b/src/backends/metal/input.rs
@@ -121,6 +121,11 @@ impl MetalBackend {
             c::LIBINPUT_EVENT_TABLET_PAD_BUTTON => self.handle_tablet_pad_button(event),
             c::LIBINPUT_EVENT_TABLET_PAD_RING => self.handle_tablet_pad_ring(event),
             c::LIBINPUT_EVENT_TABLET_PAD_STRIP => self.handle_tablet_pad_strip(event),
+            c::LIBINPUT_EVENT_TOUCH_DOWN => self.handle_touch_down(event),
+            c::LIBINPUT_EVENT_TOUCH_UP => self.handle_touch_up(event),
+            c::LIBINPUT_EVENT_TOUCH_MOTION => self.handle_touch_motion(event),
+            c::LIBINPUT_EVENT_TOUCH_CANCEL => self.handle_touch_cancel(event),
+            c::LIBINPUT_EVENT_TOUCH_FRAME => self.handle_touch_frame(event),
             _ => {}
         }
     }
@@ -539,4 +544,45 @@ impl MetalBackend {
             },
         });
     }
+
+    fn handle_touch_down(self: &Rc<Self>, event: LibInputEvent) {
+        let (event, dev) = unpack!(self, event, touch_event);
+        dev.event(InputEvent::TouchDown {
+            time_usec: event.time_usec(),
+            id: event.seat_slot(),
+            x_normed: Fixed::from_f64(event.x_transformed(1)),
+            y_normed: Fixed::from_f64(event.y_transformed(1)),
+        })
+    }
+
+    fn handle_touch_up(self: &Rc<Self>, event: LibInputEvent) {
+        let (event, dev) = unpack!(self, event, touch_event);
+        dev.event(InputEvent::TouchUp {
+            time_usec: event.time_usec(),
+            id: event.seat_slot(),
+        })
+    }
+
+    fn handle_touch_motion(self: &Rc<Self>, event: LibInputEvent) {
+        let (event, dev) = unpack!(self, event, touch_event);
+        dev.event(InputEvent::TouchMotion {
+            time_usec: event.time_usec(),
+            id: event.seat_slot(),
+            x_normed: Fixed::from_f64(event.x_transformed(1)),
+            y_normed: Fixed::from_f64(event.y_transformed(1)),
+        })
+    }
+
+    fn handle_touch_cancel(self: &Rc<Self>, event: LibInputEvent) {
+        let (event, dev) = unpack!(self, event, touch_event);
+        dev.event(InputEvent::TouchCancel {
+            time_usec: event.time_usec(),
+            id: event.seat_slot(),
+        })
+    }
+
+    fn handle_touch_frame(self: &Rc<Self>, event: LibInputEvent) {
+        let (_, dev) = unpack!(self, event, touch_event);
+        dev.event(InputEvent::TouchFrame)
+    }
 }
diff --git a/src/cli/input.rs b/src/cli/input.rs
index 280ee485..b41c2c4b 100644
--- a/src/cli/input.rs
+++ b/src/cli/input.rs
@@ -131,6 +131,8 @@ pub enum DeviceCommand {
     MapToOutput(MapToOutputArgs),
     /// Removes the mapping from this device to an output.
     RemoveMapping,
+    /// Set the calibration matrix.
+    SetCalibrationMatrix(SetCalibrationMatrixArgs),
 }
 
 #[derive(ValueEnum, Debug, Clone)]
@@ -200,6 +202,16 @@ pub struct SetTransformMatrixArgs {
     pub m22: f64,
 }
 
+#[derive(Args, Debug, Clone)]
+pub struct SetCalibrationMatrixArgs {
+    pub m00: f32,
+    pub m01: f32,
+    pub m02: f32,
+    pub m10: f32,
+    pub m11: f32,
+    pub m12: f32,
+}
+
 #[derive(Args, Debug, Clone)]
 pub struct MapToOutputArgs {
     /// The output to map to.
@@ -272,6 +284,7 @@ struct InputDevice {
     pub px_per_wheel_scroll: Option<f64>,
     pub transform_matrix: Option<[[f64; 2]; 2]>,
     pub output: Option<String>,
+    pub calibration_matrix: Option<[[f32; 3]; 2]>,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -595,6 +608,21 @@ impl Input {
                     output: None,
                 });
             }
+            DeviceCommand::SetCalibrationMatrix(a) => {
+                self.handle_error(input, |e| {
+                    eprintln!("Could not modify the calibration matrix: {}", e);
+                });
+                tc.send(jay_input::SetCalibrationMatrix {
+                    self_id: input,
+                    id: args.device,
+                    m00: a.m00,
+                    m01: a.m01,
+                    m02: a.m02,
+                    m10: a.m10,
+                    m11: a.m11,
+                    m12: a.m12,
+                });
+            }
         }
         tc.round_trip().await;
     }
@@ -728,6 +756,9 @@ impl Input {
         if let Some(v) = &device.output {
             println!("{prefix}  mapped to output: {}", v);
         }
+        if let Some(v) = &device.calibration_matrix {
+            println!("{prefix}  calibration matrix: {:?}", v);
+        }
     }
 
     async fn get(self: &Rc<Self>, input: JayInputId) -> Data {
@@ -792,6 +823,7 @@ impl Input {
                 px_per_wheel_scroll: is_pointer.then_some(msg.px_per_wheel_scroll),
                 transform_matrix: uapi::pod_read(msg.transform_matrix).ok(),
                 output: None,
+                calibration_matrix: None,
             });
         });
         jay_input::InputDeviceOutput::handle(tc, input, data.clone(), |data, msg| {
@@ -800,6 +832,13 @@ impl Input {
                 last.output = Some(msg.output.to_string());
             }
         });
+        jay_input::CalibrationMatrix::handle(tc, input, data.clone(), |data, msg| {
+            let mut data = data.borrow_mut();
+            if let Some(last) = data.input_device.last_mut() {
+                last.calibration_matrix =
+                    Some([[msg.m00, msg.m01, msg.m02], [msg.m10, msg.m11, msg.m12]]);
+            }
+        });
         tc.round_trip().await;
         let x = data.borrow_mut().clone();
         x
diff --git a/src/cli/seat_test.rs b/src/cli/seat_test.rs
index cdc8422e..8a523971 100644
--- a/src/cli/seat_test.rs
+++ b/src/cli/seat_test.rs
@@ -15,7 +15,8 @@ use {
                 TabletPadStripSource, TabletPadStripStop, TabletToolButton, TabletToolDistance,
                 TabletToolDown, TabletToolFrame, TabletToolMotion, TabletToolPressure,
                 TabletToolProximityIn, TabletToolProximityOut, TabletToolRotation,
-                TabletToolSlider, TabletToolTilt, TabletToolUp, TabletToolWheel,
+                TabletToolSlider, TabletToolTilt, TabletToolUp, TabletToolWheel, TouchCancel,
+                TouchDown, TouchMotion, TouchUp,
             },
         },
     },
@@ -583,6 +584,54 @@ async fn run(seat_test: Rc<SeatTest>) {
         }
         println!();
     });
+    let st = seat_test.clone();
+    TouchDown::handle(tc, se, (), move |_, ev| {
+        if all || ev.seat == seat {
+            if all {
+                print!("Seat: {}, ", st.name(ev.seat));
+            }
+            println!(
+                "Time: {:.4}, Touch: {}, Down: {}x{}",
+                time(ev.time_usec),
+                ev.id,
+                ev.x,
+                ev.y
+            );
+        }
+    });
+    let st = seat_test.clone();
+    TouchUp::handle(tc, se, (), move |_, ev| {
+        if all || ev.seat == seat {
+            if all {
+                print!("Seat: {}, ", st.name(ev.seat));
+            }
+            println!("Time: {:.4}, Touch: {}, Up", time(ev.time_usec), ev.id);
+        }
+    });
+    let st = seat_test.clone();
+    TouchMotion::handle(tc, se, (), move |_, ev| {
+        if all || ev.seat == seat {
+            if all {
+                print!("Seat: {}, ", st.name(ev.seat));
+            }
+            println!(
+                "Time: {:.4}, Touch: {} Motion: {}x{}",
+                time(ev.time_usec),
+                ev.id,
+                ev.x,
+                ev.y
+            );
+        }
+    });
+    let st = seat_test.clone();
+    TouchCancel::handle(tc, se, (), move |_, ev| {
+        if all || ev.seat == seat {
+            if all {
+                print!("Seat: {}, ", st.name(ev.seat));
+            }
+            println!("Time: {:.4}, Touch: {}, Cancel", time(ev.time_usec), ev.id);
+        }
+    });
     pending::<()>().await;
 }
 
diff --git a/src/config/handler.rs b/src/config/handler.rs
index 2e471910..5b9e5396 100644
--- a/src/config/handler.rs
+++ b/src/config/handler.rs
@@ -689,6 +689,16 @@ impl ConfigProxyHandler {
         Ok(())
     }
 
+    fn handle_set_calibration_matrix(
+        &self,
+        device: InputDevice,
+        matrix: [[f32; 3]; 2],
+    ) -> Result<(), CphError> {
+        let dev = self.get_device_handler_data(device)?;
+        dev.device.set_calibration_matrix(matrix);
+        Ok(())
+    }
+
     fn handle_get_workspace(&self, name: &str) {
         let name = Rc::new(name.to_owned());
         let ws = match self.workspaces_by_name.get(&name) {
@@ -1897,6 +1907,9 @@ impl ConfigProxyHandler {
             ClientMessage::SetTearingMode { connector, mode } => self
                 .handle_set_tearing_mode(connector, mode)
                 .wrn("set_tearing_mode")?,
+            ClientMessage::SetCalibrationMatrix { device, matrix } => self
+                .handle_set_calibration_matrix(device, matrix)
+                .wrn("set_calibration_matrix")?,
         }
         Ok(())
     }
diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs
index bebbf4a4..e7faef22 100644
--- a/src/ifs/jay_compositor.rs
+++ b/src/ifs/jay_compositor.rs
@@ -66,7 +66,7 @@ impl Global for JayCompositorGlobal {
     }
 
     fn version(&self) -> u32 {
-        3
+        4
     }
 
     fn required_caps(&self) -> ClientCaps {
@@ -336,7 +336,7 @@ impl JayCompositorRequestHandler for JayCompositor {
     }
 
     fn get_input(&self, req: GetInput, _slf: &Rc<Self>) -> Result<(), Self::Error> {
-        let sc = Rc::new(JayInput::new(req.id, &self.client));
+        let sc = Rc::new(JayInput::new(req.id, &self.client, self.version));
         track!(self.client, sc);
         self.client.add_client_obj(&sc)?;
         Ok(())
diff --git a/src/ifs/jay_input.rs b/src/ifs/jay_input.rs
index 605e26a8..f0362763 100644
--- a/src/ifs/jay_input.rs
+++ b/src/ifs/jay_input.rs
@@ -24,14 +24,18 @@ pub struct JayInput {
     pub id: JayInputId,
     pub client: Rc<Client>,
     pub tracker: Tracker<Self>,
+    pub version: Version,
 }
 
+const CALIBRATION_MATRIX_SINCE: Version = Version(4);
+
 impl JayInput {
-    pub fn new(id: JayInputId, client: &Rc<Client>) -> Self {
+    pub fn new(id: JayInputId, client: &Rc<Client>, version: Version) -> Self {
         Self {
             id,
             client: client.clone(),
             tracker: Default::default(),
+            version,
         }
     }
 
@@ -138,6 +142,19 @@ impl JayInput {
                 });
             }
         }
+        if self.version >= CALIBRATION_MATRIX_SINCE {
+            if let Some(m) = dev.calibration_matrix() {
+                self.client.event(CalibrationMatrix {
+                    self_id: self.id,
+                    m00: m[0][0],
+                    m01: m[0][1],
+                    m02: m[0][2],
+                    m10: m[1][0],
+                    m11: m[1][1],
+                    m12: m[1][2],
+                });
+            }
+        }
     }
 
     fn device(&self, id: u32) -> Result<Rc<DeviceHandlerData>, JayInputError> {
@@ -424,11 +441,24 @@ impl JayInputRequestHandler for JayInput {
             Ok(())
         })
     }
+
+    fn set_calibration_matrix(
+        &self,
+        req: SetCalibrationMatrix,
+        _slf: &Rc<Self>,
+    ) -> Result<(), Self::Error> {
+        self.or_error(|| {
+            let dev = self.device(req.id)?;
+            dev.device
+                .set_calibration_matrix([[req.m00, req.m01, req.m02], [req.m10, req.m11, req.m12]]);
+            Ok(())
+        })
+    }
 }
 
 object_base! {
     self = JayInput;
-    version = Version(1);
+    version = self.version;
 }
 
 impl Object for JayInput {}
diff --git a/src/ifs/jay_seat_events.rs b/src/ifs/jay_seat_events.rs
index 9df7d121..4df6980c 100644
--- a/src/ifs/jay_seat_events.rs
+++ b/src/ifs/jay_seat_events.rs
@@ -468,6 +468,46 @@ impl JaySeatEvents {
             ring,
         });
     }
+
+    pub fn send_touch_down(&self, seat: SeatId, time_usec: u64, id: i32, x: Fixed, y: Fixed) {
+        self.client.event(TouchDown {
+            self_id: self.id,
+            seat: seat.raw(),
+            time_usec,
+            id,
+            x,
+            y,
+        });
+    }
+
+    pub fn send_touch_up(&self, seat: SeatId, time_usec: u64, id: i32) {
+        self.client.event(TouchUp {
+            self_id: self.id,
+            seat: seat.raw(),
+            time_usec,
+            id,
+        });
+    }
+
+    pub fn send_touch_motion(&self, seat: SeatId, time_usec: u64, id: i32, x: Fixed, y: Fixed) {
+        self.client.event(TouchMotion {
+            self_id: self.id,
+            seat: seat.raw(),
+            time_usec,
+            id,
+            x,
+            y,
+        });
+    }
+
+    pub fn send_touch_cancel(&self, seat: SeatId, time_usec: u64, id: i32) {
+        self.client.event(TouchCancel {
+            self_id: self.id,
+            seat: seat.raw(),
+            time_usec,
+            id,
+        });
+    }
 }
 
 impl JaySeatEventsRequestHandler for JaySeatEvents {
diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs
index 7fa17c21..339a7a63 100644
--- a/src/ifs/wl_seat.rs
+++ b/src/ifs/wl_seat.rs
@@ -6,6 +6,7 @@ mod kb_owner;
 mod pointer_owner;
 pub mod tablet;
 pub mod text_input;
+mod touch_owner;
 pub mod wl_keyboard;
 pub mod wl_pointer;
 pub mod wl_touch;
@@ -52,6 +53,7 @@ use {
                     zwp_input_method_keyboard_grab_v2::ZwpInputMethodKeyboardGrabV2,
                     zwp_input_method_v2::ZwpInputMethodV2, zwp_text_input_v3::ZwpTextInputV3,
                 },
+                touch_owner::TouchOwnerHolder,
                 wl_keyboard::{WlKeyboard, WlKeyboardError, REPEAT_INFO_SINCE},
                 wl_pointer::WlPointer,
                 wl_touch::WlTouch,
@@ -79,7 +81,7 @@ use {
         },
         wire::{
             wl_seat::*, ExtIdleNotificationV1Id, WlDataDeviceId, WlKeyboardId, WlPointerId,
-            WlSeatId, ZwlrDataControlDeviceV1Id, ZwpPrimarySelectionDeviceV1Id,
+            WlSeatId, WlTouchId, ZwlrDataControlDeviceV1Id, ZwpPrimarySelectionDeviceV1Id,
             ZwpRelativePointerV1Id, ZwpTextInputV3Id,
         },
         xkbcommon::{DynKeyboardState, KeyboardState, KeymapId, XkbKeymap, XkbState},
@@ -103,7 +105,6 @@ pub use {
 
 pub const POINTER: u32 = 1;
 const KEYBOARD: u32 = 2;
-#[allow(dead_code)]
 const TOUCH: u32 = 4;
 
 #[allow(dead_code)]
@@ -142,6 +143,8 @@ pub struct WlSeatGlobal {
     name: GlobalName,
     state: Rc<State>,
     seat_name: String,
+    capabilities: Cell<u32>,
+    num_touch_devices: NumCell<u32>,
     pos_time_usec: Cell<u64>,
     pointer_stack: RefCell<Vec<Rc<dyn Node>>>,
     pointer_stack_modified: Cell<bool>,
@@ -173,6 +176,7 @@ pub struct WlSeatGlobal {
     pointer_owner: PointerOwnerHolder,
     kb_owner: KbOwnerHolder,
     gesture_owner: GestureOwnerHolder,
+    touch_owner: TouchOwnerHolder,
     dropped_dnd: RefCell<Option<DroppedDnd>>,
     shortcuts: RefCell<AHashMap<u32, SmallMap<u32, u32, 2>>>,
     queue_link: RefCell<Option<LinkedNode<Rc<Self>>>>,
@@ -213,6 +217,8 @@ impl WlSeatGlobal {
             name,
             state: state.clone(),
             seat_name: seat_name.to_string(),
+            capabilities: Cell::new(0),
+            num_touch_devices: Default::default(),
             pos_time_usec: Cell::new(0),
             pointer_stack: RefCell::new(vec![]),
             pointer_stack_modified: Cell::new(false),
@@ -237,6 +243,7 @@ impl WlSeatGlobal {
             pointer_owner: Default::default(),
             kb_owner: Default::default(),
             gesture_owner: Default::default(),
+            touch_owner: Default::default(),
             dropped_dnd: RefCell::new(None),
             shortcuts: Default::default(),
             queue_link: Default::default(),
@@ -269,9 +276,24 @@ impl WlSeatGlobal {
             }
         });
         slf.tree_changed_handler.set(Some(future));
+        slf.update_capabilities();
         slf
     }
 
+    fn update_capabilities(&self) {
+        let mut caps = POINTER | KEYBOARD;
+        if self.num_touch_devices.get() > 0 {
+            caps |= TOUCH;
+        }
+        if self.capabilities.replace(caps) != caps {
+            for client in self.bindings.borrow().values() {
+                for seat in client.values() {
+                    seat.send_capabilities();
+                }
+            }
+        }
+    }
+
     pub fn keymap(&self) -> Rc<XkbKeymap> {
         self.seat_kb_map.get()
     }
@@ -852,6 +874,7 @@ impl WlSeatGlobal {
         self.primary_selection.set(None);
         self.pointer_owner.clear();
         self.kb_owner.clear();
+        self.touch_owner.clear();
         *self.dropped_dnd.borrow_mut() = None;
         self.queue_link.take();
         self.tree_changed_handler.set(None);
@@ -888,6 +911,7 @@ impl WlSeatGlobal {
             pointers: Default::default(),
             relative_pointers: Default::default(),
             keyboards: Default::default(),
+            touches: Default::default(),
             version,
             tracker: Default::default(),
         });
@@ -994,6 +1018,7 @@ pub struct WlSeat {
     pointers: CopyHashMap<WlPointerId, Rc<WlPointer>>,
     relative_pointers: CopyHashMap<ZwpRelativePointerV1Id, Rc<ZwpRelativePointerV1>>,
     keyboards: CopyHashMap<WlKeyboardId, Rc<WlKeyboard>>,
+    touches: CopyHashMap<WlTouchId, Rc<WlTouch>>,
     version: Version,
     tracker: Tracker<Self>,
 }
@@ -1004,7 +1029,7 @@ impl WlSeat {
     fn send_capabilities(self: &Rc<Self>) {
         self.client.event(Capabilities {
             self_id: self.id,
-            capabilities: POINTER | KEYBOARD,
+            capabilities: self.global.capabilities.get(),
         })
     }
 
@@ -1059,6 +1084,7 @@ impl WlSeatRequestHandler for WlSeat {
         let p = Rc::new(WlTouch::new(req.id, slf));
         track!(self.client, p);
         self.client.add_client_obj(&p)?;
+        self.touches.set(req.id, p);
         Ok(())
     }
 
@@ -1096,6 +1122,7 @@ impl Object for WlSeat {
         self.pointers.clear();
         self.relative_pointers.clear();
         self.keyboards.clear();
+        self.touches.clear();
     }
 }
 
@@ -1146,6 +1173,10 @@ impl DeviceHandlerData {
             if let Some(info) = &self.tablet_pad_init {
                 old.tablet_remove_tablet_pad(info.id);
             }
+            if self.is_touch {
+                old.num_touch_devices.fetch_sub(1);
+                old.update_capabilities();
+            }
         }
         self.update_xkb_state();
         if let Some(seat) = &seat {
@@ -1155,6 +1186,10 @@ impl DeviceHandlerData {
             if let Some(info) = &self.tablet_pad_init {
                 seat.tablet_add_tablet_pad(self.device.id(), info);
             }
+            if self.is_touch {
+                seat.num_touch_devices.fetch_add(1);
+                seat.update_capabilities();
+            }
         }
     }
 
diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs
index 5cbbf231..ee6c55c7 100644
--- a/src/ifs/wl_seat/event_handling.rs
+++ b/src/ifs/wl_seat/event_handling.rs
@@ -24,6 +24,7 @@ use {
                     AXIS_STOP_SINCE_VERSION, AXIS_VALUE120_SINCE_VERSION, IDENTICAL, INVERTED,
                     POINTER_FRAME_SINCE_VERSION, WHEEL_TILT, WHEEL_TILT_SINCE_VERSION,
                 },
+                wl_touch::WlTouch,
                 zwp_pointer_constraints_v1::{ConstraintType, SeatConstraintStatus},
                 zwp_relative_pointer_v1::ZwpRelativePointerV1,
                 Dnd, SeatId, WlSeat, WlSeatGlobal, CHANGE_CURSOR_MOVED, CHANGE_TREE,
@@ -31,6 +32,7 @@ use {
             wl_surface::{xdg_surface::xdg_popup::XdgPopup, WlSurface},
         },
         object::Version,
+        rect::Rect,
         state::DeviceHandlerData,
         tree::{Direction, Node, ToplevelNode},
         utils::{bitflags::BitflagsExt, hash_map_ext::HashMapExt, smallmap::SmallMap},
@@ -54,6 +56,7 @@ pub struct NodeSeatState {
     pointer_foci: SmallMap<SeatId, Rc<WlSeatGlobal>, 1>,
     kb_foci: SmallMap<SeatId, Rc<WlSeatGlobal>, 1>,
     gesture_foci: SmallMap<SeatId, Rc<WlSeatGlobal>, 1>,
+    touch_foci: SmallMap<SeatId, Rc<WlSeatGlobal>, 1>,
     pointer_grabs: SmallMap<SeatId, Rc<WlSeatGlobal>, 1>,
     dnd_targets: SmallMap<SeatId, Rc<WlSeatGlobal>, 1>,
     tablet_pad_foci: SmallMap<TabletPadId, Rc<TabletPad>, 1>,
@@ -111,6 +114,14 @@ impl NodeSeatState {
         self.tablet_tool_foci.remove(&tool.id);
     }
 
+    pub(super) fn touch_begin(&self, seat: &Rc<WlSeatGlobal>) {
+        self.touch_foci.insert(seat.id, seat.clone());
+    }
+
+    pub(super) fn touch_end(&self, seat: &WlSeatGlobal) {
+        self.touch_foci.remove(&seat.id);
+    }
+
     pub(super) fn add_dnd_target(&self, seat: &Rc<WlSeatGlobal>) {
         self.dnd_targets.insert(seat.id, seat.clone());
     }
@@ -184,6 +195,9 @@ impl NodeSeatState {
         while let Some((_, pad)) = self.tablet_pad_foci.pop() {
             pad.pad_owner.focus_root(&pad);
         }
+        while let Some((_, seat)) = self.touch_foci.pop() {
+            seat.touch_owner.cancel(&seat);
+        }
         self.release_kb_focus2(focus_last);
     }
 
@@ -230,7 +244,11 @@ impl WlSeatGlobal {
             | InputEvent::TabletPadButton { time_usec, .. }
             | InputEvent::TabletPadModeSwitch { time_usec, .. }
             | InputEvent::TabletPadRing { time_usec, .. }
-            | InputEvent::TabletPadStrip { time_usec, .. } => {
+            | InputEvent::TabletPadStrip { time_usec, .. }
+            | InputEvent::TouchDown { time_usec, .. }
+            | InputEvent::TouchUp { time_usec, .. }
+            | InputEvent::TouchMotion { time_usec, .. }
+            | InputEvent::TouchCancel { time_usec, .. } => {
                 self.last_input_usec.set(time_usec);
                 if self.idle_notifications.is_not_empty() {
                     for notification in self.idle_notifications.lock().drain_values() {
@@ -243,7 +261,8 @@ impl WlSeatGlobal {
             | InputEvent::AxisStop { .. }
             | InputEvent::Axis120 { .. }
             | InputEvent::TabletToolAdded { .. }
-            | InputEvent::TabletToolRemoved { .. } => {}
+            | InputEvent::TabletToolRemoved { .. }
+            | InputEvent::TouchFrame => {}
         }
         match event {
             InputEvent::ConnectorPosition { .. }
@@ -274,6 +293,11 @@ impl WlSeatGlobal {
             InputEvent::TabletPadModeSwitch { .. } => {}
             InputEvent::TabletPadRing { .. } => {}
             InputEvent::TabletPadStrip { .. } => {}
+            InputEvent::TouchDown { .. } => {}
+            InputEvent::TouchUp { .. } => {}
+            InputEvent::TouchMotion { .. } => {}
+            InputEvent::TouchCancel { .. } => {}
+            InputEvent::TouchFrame => {}
         }
         match event {
             InputEvent::Key {
@@ -407,6 +431,21 @@ impl WlSeatGlobal {
                 source,
                 position,
             } => self.tablet_event_pad_strip(pad, strip, source, position, time_usec),
+            InputEvent::TouchDown {
+                time_usec,
+                id,
+                x_normed,
+                y_normed,
+            } => self.touch_down(time_usec, id, dev.get_rect(&self.state), x_normed, y_normed),
+            InputEvent::TouchUp { time_usec, id } => self.touch_up(time_usec, id),
+            InputEvent::TouchMotion {
+                time_usec,
+                id,
+                x_normed,
+                y_normed,
+            } => self.touch_motion(time_usec, id, dev.get_rect(&self.state), x_normed, y_normed),
+            InputEvent::TouchCancel { time_usec, id } => self.touch_cancel(time_usec, id),
+            InputEvent::TouchFrame => self.touch_frame(),
         }
     }
 
@@ -613,6 +652,58 @@ impl WlSeatGlobal {
         }
     }
 
+    fn touch_down(
+        self: &Rc<Self>,
+        time_usec: u64,
+        id: i32,
+        rect: Rect,
+        x_normed: Fixed,
+        y_normed: Fixed,
+    ) {
+        self.cursor_group().deactivate();
+        let x = Fixed::from_f64(rect.x1() as f64 + rect.width() as f64 * x_normed.to_f64());
+        let y = Fixed::from_f64(rect.y1() as f64 + rect.height() as f64 * y_normed.to_f64());
+        self.state.for_each_seat_tester(|t| {
+            t.send_touch_down(self.id, time_usec, id, x, y);
+        });
+        self.touch_owner.down(self, time_usec, id, x, y);
+    }
+
+    fn touch_up(self: &Rc<Self>, time_usec: u64, id: i32) {
+        self.state.for_each_seat_tester(|t| {
+            t.send_touch_up(self.id, time_usec, id);
+        });
+        self.touch_owner.up(self, time_usec, id);
+    }
+
+    fn touch_motion(
+        self: &Rc<Self>,
+        time_usec: u64,
+        id: i32,
+        rect: Rect,
+        x_normed: Fixed,
+        y_normed: Fixed,
+    ) {
+        self.cursor_group().deactivate();
+        let x = Fixed::from_f64(rect.x1() as f64 + rect.width() as f64 * x_normed.to_f64());
+        let y = Fixed::from_f64(rect.y1() as f64 + rect.height() as f64 * y_normed.to_f64());
+        self.state.for_each_seat_tester(|t| {
+            t.send_touch_motion(self.id, time_usec, id, x, y);
+        });
+        self.touch_owner.motion(self, time_usec, id, x, y);
+    }
+
+    fn touch_cancel(self: &Rc<Self>, time_usec: u64, id: i32) {
+        self.state.for_each_seat_tester(|t| {
+            t.send_touch_cancel(self.id, time_usec, id);
+        });
+        self.touch_owner.cancel(self);
+    }
+
+    fn touch_frame(self: &Rc<Self>) {
+        self.touch_owner.frame(self);
+    }
+
     pub(super) fn key_event<F>(
         self: &Rc<Self>,
         time_usec: u64,
@@ -744,7 +835,7 @@ impl WlSeatGlobal {
         self.kb_owner.set_kb_node(self, node);
     }
 
-    fn for_each_seat<C>(&self, ver: Version, client: ClientId, mut f: C)
+    pub(super) fn for_each_seat<C>(&self, ver: Version, client: ClientId, mut f: C)
     where
         C: FnMut(&Rc<WlSeat>),
     {
@@ -794,6 +885,18 @@ impl WlSeatGlobal {
         })
     }
 
+    fn for_each_touch<C>(&self, ver: Version, client: ClientId, mut f: C)
+    where
+        C: FnMut(&Rc<WlTouch>),
+    {
+        self.for_each_seat(ver, client, |seat| {
+            let touches = seat.touches.lock();
+            for touch in touches.values() {
+                f(touch);
+            }
+        })
+    }
+
     pub fn for_each_data_device<C>(&self, ver: Version, client: ClientId, mut f: C)
     where
         C: FnMut(&Rc<WlDataDevice>),
@@ -869,6 +972,16 @@ impl WlSeatGlobal {
         // client.flush();
     }
 
+    pub fn surface_touch_event<F>(&self, ver: Version, surface: &WlSurface, mut f: F)
+    where
+        F: FnMut(&Rc<WlTouch>),
+    {
+        let client = &surface.client;
+        self.for_each_touch(ver, client.id, |p| {
+            f(p);
+        });
+    }
+
     fn cursor_moved(self: &Rc<Self>, time_usec: u64) {
         self.pos_time_usec.set(time_usec);
         self.changes.or_assign(CHANGE_CURSOR_MOVED);
@@ -1133,6 +1246,53 @@ impl WlSeatGlobal {
     }
 }
 
+// Touch callbacks
+impl WlSeatGlobal {
+    pub fn touch_down_surface(
+        self: &Rc<Self>,
+        surface: &WlSurface,
+        time_usec: u64,
+        id: i32,
+        x: Fixed,
+        y: Fixed,
+    ) {
+        let serial = surface.client.next_serial();
+        let time = (time_usec / 1000) as _;
+        self.surface_touch_event(Version::ALL, surface, |t| {
+            t.send_down(serial, time, surface.id, id, x, y)
+        });
+        if let Some(node) = surface.get_focus_node(self.id) {
+            self.focus_node(node);
+        }
+    }
+
+    pub fn touch_up_surface(&self, surface: &WlSurface, time_usec: u64, id: i32) {
+        let serial = surface.client.next_serial();
+        let time = (time_usec / 1000) as _;
+        self.surface_touch_event(Version::ALL, surface, |t| t.send_up(serial, time, id))
+    }
+
+    pub fn touch_motion_surface(
+        &self,
+        surface: &WlSurface,
+        time_usec: u64,
+        id: i32,
+        x: Fixed,
+        y: Fixed,
+    ) {
+        let time = (time_usec / 1000) as _;
+        self.surface_touch_event(Version::ALL, surface, |t| t.send_motion(time, id, x, y));
+    }
+
+    pub fn touch_frame_surface(&self, surface: &WlSurface) {
+        self.surface_touch_event(Version::ALL, surface, |t| t.send_frame())
+    }
+
+    pub fn touch_cancel_surface(&self, surface: &WlSurface) {
+        self.surface_touch_event(Version::ALL, surface, |t| t.send_cancel())
+    }
+}
+
 // Dnd callbacks
 impl WlSeatGlobal {
     pub fn dnd_surface_leave(&self, surface: &WlSurface, dnd: &Dnd) {
diff --git a/src/ifs/wl_seat/touch_owner.rs b/src/ifs/wl_seat/touch_owner.rs
new file mode 100644
index 00000000..c3d61205
--- /dev/null
+++ b/src/ifs/wl_seat/touch_owner.rs
@@ -0,0 +1,167 @@
+use {
+    crate::{
+        fixed::Fixed,
+        ifs::wl_seat::WlSeatGlobal,
+        tree::{FindTreeUsecase, FoundNode, Node},
+        utils::{clonecell::CloneCell, smallmap::SmallMap},
+    },
+    std::rc::Rc,
+};
+
+pub struct TouchOwnerHolder {
+    default: Rc<DefaultTouchOwner>,
+    owner: CloneCell<Rc<dyn TouchOwner>>,
+}
+
+impl Default for TouchOwnerHolder {
+    fn default() -> Self {
+        Self {
+            default: Rc::new(DefaultTouchOwner),
+            owner: CloneCell::new(Rc::new(DefaultTouchOwner)),
+        }
+    }
+}
+
+impl TouchOwnerHolder {
+    pub fn down(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32, x: Fixed, y: Fixed) {
+        self.owner.get().down(seat, time_usec, id, x, y)
+    }
+
+    pub fn up(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32) {
+        self.owner.get().up(seat, time_usec, id)
+    }
+
+    pub fn motion(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32, x: Fixed, y: Fixed) {
+        self.owner.get().motion(seat, time_usec, id, x, y)
+    }
+
+    pub fn frame(&self, seat: &Rc<WlSeatGlobal>) {
+        self.owner.get().frame(seat)
+    }
+
+    pub fn cancel(&self, seat: &Rc<WlSeatGlobal>) {
+        self.owner.get().cancel(seat)
+    }
+
+    pub fn clear(&self) {
+        self.set_default_owner();
+    }
+
+    fn set_default_owner(&self) {
+        self.owner.set(self.default.clone());
+    }
+}
+
+struct DefaultTouchOwner;
+
+struct GrabTouchOwner {
+    node: Rc<dyn Node>,
+    down_ids: SmallMap<i32, (), 10>,
+}
+
+trait TouchOwner {
+    fn down(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32, x: Fixed, y: Fixed);
+    fn up(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32);
+    fn motion(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32, x: Fixed, y: Fixed);
+    fn frame(&self, seat: &Rc<WlSeatGlobal>);
+    fn cancel(&self, seat: &Rc<WlSeatGlobal>);
+}
+
+impl TouchOwner for DefaultTouchOwner {
+    fn down(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32, x: Fixed, y: Fixed) {
+        let mut found_tree = seat.found_tree.borrow_mut();
+        let x_int = x.round_down();
+        let y_int = y.round_down();
+        found_tree.push(FoundNode {
+            node: seat.state.root.clone(),
+            x: x_int,
+            y: y_int,
+        });
+        seat.state
+            .root
+            .node_find_tree_at(x_int, y_int, &mut found_tree, FindTreeUsecase::None);
+        let node = found_tree.pop();
+        found_tree.clear();
+        drop(found_tree);
+        if let Some(node) = node {
+            node.node.node_seat_state().touch_begin(seat);
+            let owner = Rc::new(GrabTouchOwner {
+                node: node.node,
+                down_ids: Default::default(),
+            });
+            seat.touch_owner.owner.set(owner.clone());
+            owner.down(seat, time_usec, id, x, y);
+        }
+    }
+
+    fn up(&self, _seat: &Rc<WlSeatGlobal>, _time_usec: u64, _id: i32) {
+        // nothing
+    }
+
+    fn motion(&self, _seat: &Rc<WlSeatGlobal>, _time_usec: u64, _id: i32, _x: Fixed, _y: Fixed) {
+        // nothing
+    }
+
+    fn frame(&self, _seat: &Rc<WlSeatGlobal>) {
+        // nothing
+    }
+
+    fn cancel(&self, _seat: &Rc<WlSeatGlobal>) {
+        // nothing
+    }
+}
+
+impl GrabTouchOwner {
+    fn translate(&self, x: Fixed, y: Fixed) -> (Fixed, Fixed) {
+        let x_int = x.round_down();
+        let y_int = y.round_down();
+        let (x_int, y_int) = self.node.node_absolute_position().translate(x_int, y_int);
+        (x.apply_fract(x_int), y.apply_fract(y_int))
+    }
+
+    fn revert_to_default(&self, seat: &Rc<WlSeatGlobal>) {
+        self.node.node_seat_state().touch_end(seat);
+        seat.touch_owner.set_default_owner();
+    }
+}
+
+impl TouchOwner for GrabTouchOwner {
+    fn down(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32, x: Fixed, y: Fixed) {
+        if self.down_ids.insert(id, ()).is_some() {
+            return;
+        }
+        let (x, y) = self.translate(x, y);
+        self.node
+            .clone()
+            .node_on_touch_down(seat, time_usec, id, x, y);
+    }
+
+    fn up(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32) {
+        if self.down_ids.remove(&id).is_none() {
+            return;
+        }
+        self.node.clone().node_on_touch_up(seat, time_usec, id);
+    }
+
+    fn motion(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32, x: Fixed, y: Fixed) {
+        if !self.down_ids.contains(&id) {
+            return;
+        }
+        let (x, y) = self.translate(x, y);
+        self.node
+            .clone()
+            .node_on_touch_motion(seat, time_usec, id, x, y);
+    }
+
+    fn frame(&self, seat: &Rc<WlSeatGlobal>) {
+        self.node.node_on_touch_frame(seat);
+        if self.down_ids.is_empty() {
+            self.revert_to_default(seat);
+        }
+    }
+
+    fn cancel(&self, seat: &Rc<WlSeatGlobal>) {
+        self.node.node_on_touch_cancel(seat);
+        self.revert_to_default(seat);
+    }
+}
diff --git a/src/ifs/wl_seat/wl_touch.rs b/src/ifs/wl_seat/wl_touch.rs
index 6a7a5a8d..384f4543 100644
--- a/src/ifs/wl_seat/wl_touch.rs
+++ b/src/ifs/wl_seat/wl_touch.rs
@@ -1,29 +1,20 @@
 use {
     crate::{
         client::ClientError,
+        fixed::Fixed,
         ifs::wl_seat::WlSeat,
         leaks::Tracker,
-        object::Object,
-        wire::{wl_touch::*, WlTouchId},
+        object::{Object, Version},
+        wire::{wl_touch::*, WlSurfaceId, WlTouchId},
     },
     std::rc::Rc,
     thiserror::Error,
 };
 
 #[allow(dead_code)]
-const DOWN: u32 = 0;
+pub const SHAPE_SINCE_VERSION: Version = Version(6);
 #[allow(dead_code)]
-const UP: u32 = 1;
-#[allow(dead_code)]
-const MOTION: u32 = 2;
-#[allow(dead_code)]
-const FRAME: u32 = 3;
-#[allow(dead_code)]
-const CANCEL: u32 = 4;
-#[allow(dead_code)]
-const SHAPE: u32 = 5;
-#[allow(dead_code)]
-const ORIENTATION: u32 = 6;
+pub const ORIENTATION_DIRECTION_SINCE_VERSION: Version = Version(6);
 
 pub struct WlTouch {
     id: WlTouchId,
@@ -39,12 +30,79 @@ impl WlTouch {
             tracker: Default::default(),
         }
     }
+
+    pub fn send_down(
+        &self,
+        serial: u32,
+        time: u32,
+        surface: WlSurfaceId,
+        id: i32,
+        x: Fixed,
+        y: Fixed,
+    ) {
+        self.seat.client.event(Down {
+            self_id: self.id,
+            serial,
+            time,
+            surface,
+            id,
+            x,
+            y,
+        })
+    }
+
+    pub fn send_up(&self, serial: u32, time: u32, id: i32) {
+        self.seat.client.event(Up {
+            self_id: self.id,
+            serial,
+            time,
+            id,
+        })
+    }
+
+    pub fn send_motion(&self, time: u32, id: i32, x: Fixed, y: Fixed) {
+        self.seat.client.event(Motion {
+            self_id: self.id,
+            time,
+            id,
+            x,
+            y,
+        })
+    }
+
+    pub fn send_frame(&self) {
+        self.seat.client.event(Frame { self_id: self.id })
+    }
+
+    pub fn send_cancel(&self) {
+        self.seat.client.event(Cancel { self_id: self.id })
+    }
+
+    #[allow(dead_code)]
+    pub fn send_shape(&self, id: i32, major: Fixed, minor: Fixed) {
+        self.seat.client.event(Shape {
+            self_id: self.id,
+            id,
+            major,
+            minor,
+        })
+    }
+
+    #[allow(dead_code)]
+    pub fn send_orientation(&self, id: i32, orientation: Fixed) {
+        self.seat.client.event(Orientation {
+            self_id: self.id,
+            id,
+            orientation,
+        })
+    }
 }
 
 impl WlTouchRequestHandler for WlTouch {
     type Error = WlTouchError;
 
     fn release(&self, _req: Release, _slf: &Rc<Self>) -> Result<(), Self::Error> {
+        self.seat.touches.remove(&self.id);
         self.seat.client.remove_obj(self)?;
         Ok(())
     }
diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs
index 77dd8165..049c5034 100644
--- a/src/ifs/wl_surface.rs
+++ b/src/ifs/wl_surface.rs
@@ -1643,6 +1643,40 @@ impl Node for WlSurface {
         seat.mods_surface(self, kb_state);
     }
 
+    fn node_on_touch_down(
+        self: Rc<Self>,
+        seat: &Rc<WlSeatGlobal>,
+        time_usec: u64,
+        id: i32,
+        x: Fixed,
+        y: Fixed,
+    ) {
+        seat.touch_down_surface(&self, time_usec, id, x, y)
+    }
+
+    fn node_on_touch_up(self: Rc<Self>, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32) {
+        seat.touch_up_surface(&self, time_usec, id)
+    }
+
+    fn node_on_touch_motion(
+        self: Rc<Self>,
+        seat: &WlSeatGlobal,
+        time_usec: u64,
+        id: i32,
+        x: Fixed,
+        y: Fixed,
+    ) {
+        seat.touch_motion_surface(&self, time_usec, id, x, y)
+    }
+
+    fn node_on_touch_frame(&self, seat: &WlSeatGlobal) {
+        seat.touch_frame_surface(&self)
+    }
+
+    fn node_on_touch_cancel(&self, seat: &WlSeatGlobal) {
+        seat.touch_cancel_surface(&self)
+    }
+
     fn node_on_button(
         self: Rc<Self>,
         seat: &Rc<WlSeatGlobal>,
diff --git a/src/libinput/device.rs b/src/libinput/device.rs
index 76f813f9..bf217781 100644
--- a/src/libinput/device.rs
+++ b/src/libinput/device.rs
@@ -10,7 +10,9 @@ use {
             libinput_device, libinput_device_config_accel_get_profile,
             libinput_device_config_accel_get_speed, libinput_device_config_accel_is_available,
             libinput_device_config_accel_set_profile, libinput_device_config_accel_set_speed,
-            libinput_device_config_left_handed_get,
+            libinput_device_config_calibration_get_matrix,
+            libinput_device_config_calibration_has_matrix,
+            libinput_device_config_calibration_set_matrix, libinput_device_config_left_handed_get,
             libinput_device_config_left_handed_is_available,
             libinput_device_config_left_handed_set,
             libinput_device_config_scroll_get_natural_scroll_enabled,
@@ -265,6 +267,25 @@ impl<'a> LibInputDevice<'a> {
             _phantom: Default::default(),
         })
     }
+
+    pub fn has_calibration_matrix(&self) -> bool {
+        unsafe { libinput_device_config_calibration_has_matrix(self.dev) != 0 }
+    }
+
+    pub fn set_calibration_matrix(&self, m: [[f32; 3]; 2]) {
+        let m = [m[0][0], m[0][1], m[0][2], m[1][0], m[1][1], m[1][2]];
+        unsafe {
+            libinput_device_config_calibration_set_matrix(self.dev, &m);
+        }
+    }
+
+    pub fn get_calibration_matrix(&self) -> [[f32; 3]; 2] {
+        let mut m = [0.0; 6];
+        unsafe {
+            libinput_device_config_calibration_get_matrix(self.dev, &mut m);
+        }
+        [[m[0], m[1], m[2]], [m[3], m[4], m[5]]]
+    }
 }
 
 impl<'a> LibInputDeviceGroup<'a> {
diff --git a/src/libinput/event.rs b/src/libinput/event.rs
index 45885c48..3048a0fb 100644
--- a/src/libinput/event.rs
+++ b/src/libinput/event.rs
@@ -16,17 +16,17 @@ use {
             libinput_event_get_gesture_event, libinput_event_get_keyboard_event,
             libinput_event_get_pointer_event, libinput_event_get_switch_event,
             libinput_event_get_tablet_pad_event, libinput_event_get_tablet_tool_event,
-            libinput_event_get_type, libinput_event_keyboard, libinput_event_keyboard_get_key,
-            libinput_event_keyboard_get_key_state, libinput_event_keyboard_get_time_usec,
-            libinput_event_pointer, libinput_event_pointer_get_button,
-            libinput_event_pointer_get_button_state, libinput_event_pointer_get_dx,
-            libinput_event_pointer_get_dx_unaccelerated, libinput_event_pointer_get_dy,
-            libinput_event_pointer_get_dy_unaccelerated, libinput_event_pointer_get_scroll_value,
-            libinput_event_pointer_get_scroll_value_v120, libinput_event_pointer_get_time_usec,
-            libinput_event_pointer_has_axis, libinput_event_switch,
-            libinput_event_switch_get_switch, libinput_event_switch_get_switch_state,
-            libinput_event_switch_get_time_usec, libinput_event_tablet_pad,
-            libinput_event_tablet_pad_get_button_number,
+            libinput_event_get_touch_event, libinput_event_get_type, libinput_event_keyboard,
+            libinput_event_keyboard_get_key, libinput_event_keyboard_get_key_state,
+            libinput_event_keyboard_get_time_usec, libinput_event_pointer,
+            libinput_event_pointer_get_button, libinput_event_pointer_get_button_state,
+            libinput_event_pointer_get_dx, libinput_event_pointer_get_dx_unaccelerated,
+            libinput_event_pointer_get_dy, libinput_event_pointer_get_dy_unaccelerated,
+            libinput_event_pointer_get_scroll_value, libinput_event_pointer_get_scroll_value_v120,
+            libinput_event_pointer_get_time_usec, libinput_event_pointer_has_axis,
+            libinput_event_switch, libinput_event_switch_get_switch,
+            libinput_event_switch_get_switch_state, libinput_event_switch_get_time_usec,
+            libinput_event_tablet_pad, libinput_event_tablet_pad_get_button_number,
             libinput_event_tablet_pad_get_button_state, libinput_event_tablet_pad_get_mode,
             libinput_event_tablet_pad_get_mode_group, libinput_event_tablet_pad_get_ring_number,
             libinput_event_tablet_pad_get_ring_position, libinput_event_tablet_pad_get_ring_source,
@@ -40,10 +40,12 @@ use {
             libinput_event_tablet_tool_get_tool,
             libinput_event_tablet_tool_get_wheel_delta_discrete,
             libinput_event_tablet_tool_get_x_transformed,
-            libinput_event_tablet_tool_get_y_transformed, libinput_tablet_tool,
-            libinput_tablet_tool_get_serial, libinput_tablet_tool_get_tool_id,
-            libinput_tablet_tool_get_type, libinput_tablet_tool_get_user_data,
-            libinput_tablet_tool_set_user_data,
+            libinput_event_tablet_tool_get_y_transformed, libinput_event_touch,
+            libinput_event_touch_get_seat_slot, libinput_event_touch_get_time_usec,
+            libinput_event_touch_get_x_transformed, libinput_event_touch_get_y_transformed,
+            libinput_tablet_tool, libinput_tablet_tool_get_serial,
+            libinput_tablet_tool_get_tool_id, libinput_tablet_tool_get_type,
+            libinput_tablet_tool_get_user_data, libinput_tablet_tool_set_user_data,
         },
     },
     std::marker::PhantomData,
@@ -89,6 +91,11 @@ pub struct LibInputTabletTool<'a> {
     pub(super) _phantom: PhantomData<&'a ()>,
 }
 
+pub struct LibInputEventTouch<'a> {
+    pub(super) event: *mut libinput_event_touch,
+    pub(super) _phantom: PhantomData<&'a ()>,
+}
+
 impl<'a> Drop for LibInputEvent<'a> {
     fn drop(&mut self) {
         unsafe {
@@ -155,6 +162,11 @@ impl<'a> LibInputEvent<'a> {
         LibInputEventTabletPad,
         libinput_event_get_tablet_pad_event
     );
+    converter!(
+        touch_event,
+        LibInputEventTouch,
+        libinput_event_get_touch_event
+    );
 }
 
 impl<'a> LibInputEventKeyboard<'a> {
@@ -467,3 +479,29 @@ impl<'a> LibInputEventTabletPad<'a> {
         }
     }
 }
+
+impl<'a> LibInputEventTouch<'a> {
+    pub fn seat_slot(&self) -> i32 {
+        unsafe { libinput_event_touch_get_seat_slot(self.event) }
+    }
+
+    // pub fn x(&self) -> f64 {
+    //     unsafe { libinput_event_touch_get_x(self.event) }
+    // }
+    //
+    // pub fn y(&self) -> f64 {
+    //     unsafe { libinput_event_touch_get_y(self.event) }
+    // }
+
+    pub fn x_transformed(&self, width: u32) -> f64 {
+        unsafe { libinput_event_touch_get_x_transformed(self.event, width) }
+    }
+
+    pub fn y_transformed(&self, height: u32) -> f64 {
+        unsafe { libinput_event_touch_get_y_transformed(self.event, height) }
+    }
+
+    pub fn time_usec(&self) -> u64 {
+        unsafe { libinput_event_touch_get_time_usec(self.event) }
+    }
+}
diff --git a/src/libinput/sys.rs b/src/libinput/sys.rs
index c937abb3..56f7a7c8 100644
--- a/src/libinput/sys.rs
+++ b/src/libinput/sys.rs
@@ -30,6 +30,8 @@ pub struct libinput_tablet_pad_mode_group(u8);
 pub struct libinput_tablet_tool(u8);
 // #[repr(transparent)]
 // pub struct libinput_tablet_pad(u8);
+#[repr(transparent)]
+pub struct libinput_event_touch(u8);
 
 #[link(name = "input")]
 extern "C" {
@@ -357,6 +359,29 @@ extern "C" {
     //     group: *mut libinput_tablet_pad_mode_group,
     //     button: c::c_uint,
     // ) -> c::c_int;
+
+    pub fn libinput_event_get_touch_event(event: *mut libinput_event) -> *mut libinput_event_touch;
+    pub fn libinput_event_touch_get_seat_slot(event: *mut libinput_event_touch) -> i32;
+    pub fn libinput_event_touch_get_time_usec(event: *mut libinput_event_touch) -> u64;
+    // pub fn libinput_event_touch_get_x(event: *mut libinput_event_touch) -> f64;
+    pub fn libinput_event_touch_get_x_transformed(
+        event: *mut libinput_event_touch,
+        width: u32,
+    ) -> f64;
+    // pub fn libinput_event_touch_get_y(event: *mut libinput_event_touch) -> f64;
+    pub fn libinput_event_touch_get_y_transformed(
+        event: *mut libinput_event_touch,
+        height: u32,
+    ) -> f64;
+    pub fn libinput_device_config_calibration_has_matrix(device: *mut libinput_device) -> c::c_int;
+    pub fn libinput_device_config_calibration_set_matrix(
+        device: *mut libinput_device,
+        matrix: *const [f32; 6],
+    ) -> libinput_config_status;
+    pub fn libinput_device_config_calibration_get_matrix(
+        device: *mut libinput_device,
+        matrix: *mut [f32; 6],
+    ) -> c::c_int;
 }
 
 #[repr(C)]
diff --git a/src/state.rs b/src/state.rs
index 7af22022..5318ab8d 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -278,6 +278,7 @@ pub struct DeviceHandlerData {
     pub output: CloneCell<Option<Rc<OutputGlobalOpt>>>,
     pub tablet_init: Option<Box<TabletInit>>,
     pub tablet_pad_init: Option<Box<TabletPadInit>>,
+    pub is_touch: bool,
 }
 
 pub struct ConnectorData {
diff --git a/src/tasks/input_device.rs b/src/tasks/input_device.rs
index 3404d763..d255a5c1 100644
--- a/src/tasks/input_device.rs
+++ b/src/tasks/input_device.rs
@@ -1,6 +1,6 @@
 use {
     crate::{
-        backend::InputDevice,
+        backend::{InputDevice, InputDeviceCapability},
         ifs::wl_seat::PX_PER_SCROLL,
         state::{DeviceHandlerData, InputDeviceData, State},
         tasks::udev_utils::{udev_props, UdevProps},
@@ -26,6 +26,7 @@ pub fn handle(state: &Rc<State>, dev: Rc<dyn InputDevice>) {
         output: Default::default(),
         tablet_init: dev.tablet_info(),
         tablet_pad_init: dev.tablet_pad_info(),
+        is_touch: dev.has_capability(InputDeviceCapability::Touch),
     });
     let ae = Rc::new(AsyncEvent::default());
     let oh = DeviceHandler {
diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs
index a9ed6216..5468132a 100644
--- a/src/tools/tool_client.rs
+++ b/src/tools/tool_client.rs
@@ -330,7 +330,7 @@ impl ToolClient {
             self_id: s.registry,
             name: s.jay_compositor.0,
             interface: JayCompositor.name(),
-            version: s.jay_compositor.1.min(3),
+            version: s.jay_compositor.1.min(4),
             id: id.into(),
         });
         self.jay_compositor.set(Some(id));
diff --git a/src/tree.rs b/src/tree.rs
index 58f679f0..797820c1 100644
--- a/src/tree.rs
+++ b/src/tree.rs
@@ -200,6 +200,50 @@ pub trait Node: 'static {
         let _ = kb_state;
     }
 
+    fn node_on_touch_down(
+        self: Rc<Self>,
+        seat: &Rc<WlSeatGlobal>,
+        time_usec: u64,
+        id: i32,
+        x: Fixed,
+        y: Fixed,
+    ) {
+        let _ = seat;
+        let _ = time_usec;
+        let _ = id;
+        let _ = x;
+        let _ = y;
+    }
+
+    fn node_on_touch_up(self: Rc<Self>, seat: &Rc<WlSeatGlobal>, time_usec: u64, id: i32) {
+        let _ = seat;
+        let _ = time_usec;
+        let _ = id;
+    }
+
+    fn node_on_touch_motion(
+        self: Rc<Self>,
+        seat: &WlSeatGlobal,
+        time_usec: u64,
+        id: i32,
+        x: Fixed,
+        y: Fixed,
+    ) {
+        let _ = seat;
+        let _ = time_usec;
+        let _ = id;
+        let _ = x;
+        let _ = y;
+    }
+
+    fn node_on_touch_frame(&self, seat: &WlSeatGlobal) {
+        let _ = seat;
+    }
+
+    fn node_on_touch_cancel(&self, seat: &WlSeatGlobal) {
+        let _ = seat;
+    }
+
     fn node_on_button(
         self: Rc<Self>,
         seat: &Rc<WlSeatGlobal>,
diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs
index 625787b2..e2fc7532 100644
--- a/toml-config/src/config.rs
+++ b/toml-config/src/config.rs
@@ -250,6 +250,7 @@ pub struct Input {
     pub keymap: Option<ConfigKeymap>,
     pub switch_actions: AHashMap<SwitchEvent, Action>,
     pub output: Option<Option<OutputMatch>>,
+    pub calibration_matrix: Option<[[f32; 3]; 2]>,
 }
 
 #[derive(Debug, Clone)]
diff --git a/toml-config/src/config/parsers/input.rs b/toml-config/src/config/parsers/input.rs
index 3b5ff6b8..dbe78335 100644
--- a/toml-config/src/config/parsers/input.rs
+++ b/toml-config/src/config/parsers/input.rs
@@ -40,6 +40,12 @@ pub enum InputParserError {
     TwoColumns,
     #[error("Transform matrix entries must be floats")]
     Float,
+    #[error("Calibration matrix must have exactly two rows")]
+    CaliTwoRows,
+    #[error("Calibration matrix must have exactly three columns")]
+    CaliThreeColumns,
+    #[error("Calibration matrix entries must be floats")]
+    CaliFloat,
 }
 
 pub struct InputParser<'a> {
@@ -80,6 +86,7 @@ impl<'a> Parser for InputParser<'a> {
                 on_converted_to_tablet_val,
                 output_val,
                 remove_mapping,
+                calibration_matrix,
             ),
         ) = ext.extract((
             (
@@ -103,6 +110,7 @@ impl<'a> Parser for InputParser<'a> {
                 opt(val("on-converted-to-tablet")),
                 opt(val("output")),
                 recover(opt(bol("remove-mapping"))),
+                recover(opt(val("calibration-matrix"))),
             ),
         ))?;
         let accel_profile = match accel_profile {
@@ -214,6 +222,16 @@ impl<'a> Parser for InputParser<'a> {
                 output = Some(None);
             }
         }
+        let calibration_matrix = match calibration_matrix {
+            None => None,
+            Some(matrix) => match matrix.parse(&mut CalibrationMatrixParser) {
+                Ok(v) => Some(v),
+                Err(e) => {
+                    log::warn!("Could not parse calibration matrix: {}", self.cx.error(e));
+                    None
+                }
+            },
+        };
         Ok(Input {
             tag: tag.despan_into(),
             match_: match_val.parse_map(&mut InputMatchParser(self.cx))?,
@@ -229,6 +247,7 @@ impl<'a> Parser for InputParser<'a> {
             keymap,
             switch_actions,
             output,
+            calibration_matrix,
         })
     }
 }
@@ -311,3 +330,45 @@ impl Parser for TransformMatrixRowParser {
         Ok([extract(&array[0])?, extract(&array[1])?])
     }
 }
+
+struct CalibrationMatrixParser;
+
+impl Parser for CalibrationMatrixParser {
+    type Value = [[f32; 3]; 2];
+    type Error = InputParserError;
+    const EXPECTED: &'static [DataType] = &[DataType::Array];
+
+    fn parse_array(&mut self, span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
+        if array.len() != 2 {
+            return Err(InputParserError::CaliTwoRows.spanned(span));
+        }
+        Ok([
+            array[0].parse(&mut CalibrationMatrixRowParser)?,
+            array[1].parse(&mut CalibrationMatrixRowParser)?,
+        ])
+    }
+}
+
+struct CalibrationMatrixRowParser;
+
+impl Parser for CalibrationMatrixRowParser {
+    type Value = [f32; 3];
+    type Error = InputParserError;
+    const EXPECTED: &'static [DataType] = &[DataType::Array];
+
+    fn parse_array(&mut self, span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
+        if array.len() != 3 {
+            return Err(InputParserError::CaliThreeColumns.spanned(span));
+        }
+        let extract = |v: &Spanned<Value>| match v.value {
+            Value::Float(f) => Ok(f as f32),
+            Value::Integer(f) => Ok(f as _),
+            _ => Err(InputParserError::CaliFloat.spanned(v.span)),
+        };
+        Ok([
+            extract(&array[0])?,
+            extract(&array[1])?,
+            extract(&array[2])?,
+        ])
+    }
+}
diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs
index 4651bf7e..da6b25a3 100644
--- a/toml-config/src/lib.rs
+++ b/toml-config/src/lib.rs
@@ -437,6 +437,9 @@ impl Input {
                 c.remove_mapping();
             }
         }
+        if let Some(v) = self.calibration_matrix {
+            c.set_calibration_matrix(v);
+        }
     }
 }
 
diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json
index 4b335ee2..04cf12e7 100644
--- a/toml-spec/spec/spec.generated.json
+++ b/toml-spec/spec/spec.generated.json
@@ -861,6 +861,18 @@
         "remove-mapping": {
           "type": "boolean",
           "description": "Removes the mapping of from this device to an output.\n\nThis should only be used within `configure-input` actions.\n\n- Example:\n\n  ```toml\n  [shortcuts]\n  alt-x = { type = \"configure-input\", input = { match.tag = \"wacom\", remove-mapping = true } }\n\n  [[inputs]]\n  tag = \"wacom\"\n  match.name = \"Wacom Bamboo Comic 2FG Pen\"\n  output.connector = \"DP-1\"\n  ```\n"
+        },
+        "calibration-matrix": {
+          "type": "array",
+          "description": "The calibration matrix of the device. This matrix should be 2x3.\n\nSee the libinput documentation for more details.\n\n- Example: To flip the device 90 degrees:\n\n  ```toml\n  [[inputs]]\n  calibration-matrix = [[0, 1, 0], [-1, 0, 1]]\n  ```\n",
+          "items": {
+            "type": "array",
+            "description": "",
+            "items": {
+              "type": "number",
+              "description": ""
+            }
+          }
         }
       },
       "required": [
diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md
index 7e615924..95d5adf4 100644
--- a/toml-spec/spec/spec.generated.md
+++ b/toml-spec/spec/spec.generated.md
@@ -1768,6 +1768,21 @@ The table has the following fields:
 
   The value of this field should be a boolean.
 
+- `calibration-matrix` (optional):
+
+  The calibration matrix of the device. This matrix should be 2x3.
+  
+  See the libinput documentation for more details.
+  
+  - Example: To flip the device 90 degrees:
+  
+    ```toml
+    [[inputs]]
+    calibration-matrix = [[0, 1, 0], [-1, 0, 1]]
+    ```
+
+  The value of this field should be an array of arrays of numbers.
+
 
 <a name="types-InputMatch"></a>
 ### `InputMatch`
diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml
index 7b16e0e8..190de9c5 100644
--- a/toml-spec/spec/spec.yaml
+++ b/toml-spec/spec/spec.yaml
@@ -1328,6 +1328,24 @@ Input:
           match.name = "Wacom Bamboo Comic 2FG Pen"
           output.connector = "DP-1"
           ```
+    calibration-matrix:
+      kind: array
+      items:
+        kind: array
+        items:
+          kind: number
+      required: false
+      description: |
+        The calibration matrix of the device. This matrix should be 2x3.
+        
+        See the libinput documentation for more details.
+        
+        - Example: To flip the device 90 degrees:
+        
+          ```toml
+          [[inputs]]
+          calibration-matrix = [[0, 1, 0], [-1, 0, 1]]
+          ```
 
 
 AccelProfile:
diff --git a/wire/jay_input.txt b/wire/jay_input.txt
index 91a7875d..c5469936 100644
--- a/wire/jay_input.txt
+++ b/wire/jay_input.txt
@@ -114,6 +114,16 @@ request map_to_output {
     output: optstr,
 }
 
+request set_calibration_matrix (since = 4) {
+    id: u32,
+    m00: pod(f32),
+    m01: pod(f32),
+    m02: pod(f32),
+    m10: pod(f32),
+    m11: pod(f32),
+    m12: pod(f32),
+}
+
 # events
 
 event seat {
@@ -158,3 +168,12 @@ event input_device_output {
     id: u32,
     output: str,
 }
+
+event calibration_matrix (since = 4) {
+    m00: pod(f32),
+    m01: pod(f32),
+    m02: pod(f32),
+    m10: pod(f32),
+    m11: pod(f32),
+    m12: pod(f32),
+}
diff --git a/wire/jay_seat_events.txt b/wire/jay_seat_events.txt
index f7cb0163..70e01eab 100644
--- a/wire/jay_seat_events.txt
+++ b/wire/jay_seat_events.txt
@@ -239,3 +239,31 @@ event tablet_pad_ring_frame {
     input_device: u32,
     ring: u32,
 }
+
+event touch_down {
+    seat: u32,
+    time_usec: pod(u64),
+    id: i32,
+    x: fixed,
+    y: fixed,
+}
+
+event touch_up {
+    seat: u32,
+    time_usec: pod(u64),
+    id: i32,
+}
+
+event touch_motion {
+    seat: u32,
+    time_usec: pod(u64),
+    id: i32,
+    x: fixed,
+    y: fixed,
+}
+
+event touch_cancel {
+    seat: u32,
+    time_usec: pod(u64),
+    id: i32,
+}
diff --git a/wire/wl_touch.txt b/wire/wl_touch.txt
index 11ec77dd..a5a21c3f 100644
--- a/wire/wl_touch.txt
+++ b/wire/wl_touch.txt
@@ -23,7 +23,7 @@ event up {
 
 event motion {
     time: u32,
-    id: u32,
+    id: i32,
     x: fixed,
     y: fixed,
 }