A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4#if NET5_0
5using OpenTabletDriver.Plugin;
6using OpenTabletDriver.Plugin.Output;
7using OpenTabletDriver.Plugin.Platform.Pointer;
8using OpenTabletDriver.Plugin.Tablet;
9using osu.Framework.Bindables;
10using osu.Framework.Input.StateChanges;
11using osu.Framework.Platform;
12using osu.Framework.Statistics;
13using osuTK;
14
15namespace osu.Framework.Input.Handlers.Tablet
16{
17 public class OpenTabletDriverHandler : InputHandler, IAbsolutePointer, IVirtualTablet, IRelativePointer, ITabletHandler
18 {
19 public override bool IsActive => tabletDriver.EnableInput;
20
21 private TabletDriver tabletDriver;
22
23 public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();
24
25 public Bindable<Vector2> AreaSize { get; } = new Bindable<Vector2>();
26
27 public Bindable<float> Rotation { get; } = new Bindable<float>();
28
29 public IBindable<TabletInfo> Tablet => tablet;
30
31 private readonly Bindable<TabletInfo> tablet = new Bindable<TabletInfo>();
32
33 public override bool Initialize(GameHost host)
34 {
35 tabletDriver = new TabletDriver
36 {
37 // for now let's keep things simple and always use absolute mode.
38 // this will likely be a user setting in the future.
39 OutputMode = new AbsoluteTabletMode(this)
40 };
41
42 updateOutputArea(host.Window);
43
44 host.Window.Resized += () => updateOutputArea(host.Window);
45
46 AreaOffset.BindValueChanged(_ => updateInputArea());
47 AreaSize.BindValueChanged(_ => updateInputArea(), true);
48 Rotation.BindValueChanged(_ => updateInputArea(), true);
49
50 tabletDriver.TabletChanged += (sender, e) => updateInputArea();
51 tabletDriver.ReportReceived += (sender, report) =>
52 {
53 switch (report)
54 {
55 case ITabletReport tabletReport:
56 handleTabletReport(tabletReport);
57 break;
58
59 case IAuxReport auxiliaryReport:
60 handleAuxiliaryReport(auxiliaryReport);
61 break;
62 }
63 };
64
65 Enabled.BindValueChanged(d =>
66 {
67 if (d.NewValue)
68 {
69 if (tabletDriver.Tablet == null)
70 tabletDriver.DetectTablet();
71 }
72
73 tabletDriver.EnableInput = d.NewValue;
74 }, true);
75
76 return true;
77 }
78
79 void IAbsolutePointer.SetPosition(System.Numerics.Vector2 pos) => enqueueInput(new MousePositionAbsoluteInput { Position = new Vector2(pos.X, pos.Y) });
80
81 void IVirtualTablet.SetPressure(float percentage) => enqueueInput(new MouseButtonInput(osuTK.Input.MouseButton.Left, percentage > 0));
82
83 void IRelativePointer.Translate(System.Numerics.Vector2 delta) => enqueueInput(new MousePositionRelativeInput { Delta = new Vector2(delta.X, delta.Y) });
84
85 private void updateOutputArea(IWindow window)
86 {
87 switch (tabletDriver.OutputMode)
88 {
89 case AbsoluteOutputMode absoluteOutputMode:
90 {
91 float outputWidth, outputHeight;
92
93 // Set output area in pixels
94 absoluteOutputMode.Output = new Area
95 {
96 Width = outputWidth = window.ClientSize.Width,
97 Height = outputHeight = window.ClientSize.Height,
98 Position = new System.Numerics.Vector2(outputWidth / 2, outputHeight / 2)
99 };
100 break;
101 }
102 }
103 }
104
105 private void updateInputArea()
106 {
107 if (tabletDriver.Tablet == null)
108 {
109 tablet.Value = null;
110 return;
111 }
112
113 float inputWidth = tabletDriver.Tablet.Digitizer.Width;
114 float inputHeight = tabletDriver.Tablet.Digitizer.Height;
115
116 AreaSize.Default = new Vector2(inputWidth, inputHeight);
117
118 // if it's clear the user has not configured the area, take the full area from the tablet that was just found.
119 if (AreaSize.Value == Vector2.Zero)
120 AreaSize.SetDefault();
121
122 AreaOffset.Default = new Vector2(inputWidth / 2, inputHeight / 2);
123
124 // likewise with the position, use the centre point if it has not been configured.
125 // it's safe to assume no user would set their centre point to 0,0 for now.
126 if (AreaOffset.Value == Vector2.Zero)
127 AreaOffset.SetDefault();
128
129 tablet.Value = new TabletInfo(tabletDriver.Tablet.TabletProperties.Name, AreaSize.Default);
130
131 switch (tabletDriver.OutputMode)
132 {
133 case AbsoluteOutputMode absoluteOutputMode:
134 {
135 // Set input area in millimeters
136 absoluteOutputMode.Input = new Area
137 {
138 Width = AreaSize.Value.X,
139 Height = AreaSize.Value.Y,
140 Position = new System.Numerics.Vector2(AreaOffset.Value.X, AreaOffset.Value.Y),
141 Rotation = Rotation.Value
142 };
143 break;
144 }
145 }
146 }
147
148 private void handleTabletReport(ITabletReport tabletReport)
149 {
150 int buttonCount = tabletReport.PenButtons.Length;
151 var buttons = new ButtonInputEntry<TabletPenButton>[buttonCount];
152 for (int i = 0; i < buttonCount; i++)
153 buttons[i] = new ButtonInputEntry<TabletPenButton>((TabletPenButton)i, tabletReport.PenButtons[i]);
154
155 enqueueInput(new TabletPenButtonInput(buttons));
156 }
157
158 private void handleAuxiliaryReport(IAuxReport auxiliaryReport)
159 {
160 int buttonCount = auxiliaryReport.AuxButtons.Length;
161 var buttons = new ButtonInputEntry<TabletAuxiliaryButton>[buttonCount];
162 for (int i = 0; i < buttonCount; i++)
163 buttons[i] = new ButtonInputEntry<TabletAuxiliaryButton>((TabletAuxiliaryButton)i, auxiliaryReport.AuxButtons[i]);
164
165 enqueueInput(new TabletAuxiliaryButtonInput(buttons));
166 }
167
168 private void enqueueInput(IInput input)
169 {
170 PendingInputs.Enqueue(input);
171 FrameStatistics.Increment(StatisticsCounterType.TabletEvents);
172 }
173 }
174}
175#endif