Skip to content

Commit c496208

Browse files
amylizzlewixoaGit
andauthored
Implement custom & default mouse cursors (OpenDreamProject#2403)
Co-authored-by: amylizzle <amylizzle@users.noreply.github.com> Co-authored-by: wixoaGit <wixoag@gmail.com>
1 parent af84c70 commit c496208

File tree

18 files changed

+287
-19
lines changed

18 files changed

+287
-19
lines changed

DMCompiler/DMStandard/Types/Atoms/_Atom.dm

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@
5656
var/step_x as opendream_unimplemented
5757
var/step_y as opendream_unimplemented
5858
var/render_source
59-
var/tmp/mouse_drag_pointer as opendream_unimplemented
60-
var/tmp/mouse_drop_pointer as opendream_unimplemented
61-
var/tmp/mouse_over_pointer as opendream_unimplemented
59+
var/tmp/mouse_drag_pointer
60+
var/tmp/mouse_drop_pointer = MOUSE_ACTIVE_POINTER
61+
var/tmp/mouse_over_pointer
62+
var/mouse_drop_zone = FALSE
6263
var/render_target
6364
var/vis_flags as opendream_unimplemented
6465

DMCompiler/DMStandard/Types/Client.dm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
var/script as opendream_unimplemented
4141
var/color = 0 as opendream_unimplemented
4242
var/control_freak as opendream_unimplemented
43-
var/mouse_pointer_icon as opendream_unimplemented
43+
var/mouse_pointer_icon
4444
var/preload_rsc = 1 as opendream_unimplemented
4545
var/fps = 0 as opendream_unimplemented
4646
var/dir = NORTH as opendream_unimplemented

DMCompiler/DMStandard/Types/Image.dm

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
var/maptext_height = 32
2323
var/maptext_x = 0
2424
var/maptext_y = 0
25-
var/mouse_over_pointer = 0 as opendream_unimplemented
26-
var/mouse_drag_pointer = 0 as opendream_unimplemented
27-
var/mouse_drop_pointer = 1 as opendream_unimplemented
28-
var/mouse_drop_zone = 0 as opendream_unimplemented
25+
var/mouse_over_pointer = 0
26+
var/mouse_drag_pointer = 0
27+
var/mouse_drop_pointer = 1
28+
var/mouse_drop_zone = 0
2929
var/mouse_opacity = 1
3030
var/name = "image"
3131
var/opacity = 0 as opendream_unimplemented

OpenDream.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
<File Path="TestGame/icons/mob.dmi" />
5757
<File Path="TestGame/icons/turf.dmi" />
5858
<File Path="TestGame/icons/bee.dmi" />
59+
<File Path="TestGame/icons/objects.dmi" />
5960
</Folder>
6061
<Folder Name="/TestGame/icons/effects/">
6162
<File Path="TestGame/icons/effects/effects.dmi" />

OpenDreamClient/Input/MouseInputSystem.cs

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ internal sealed class MouseInputSystem : SharedMouseInputSystem {
2727
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
2828
[Dependency] private readonly IDreamInterfaceManager _dreamInterfaceManager = default!;
2929
[Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!;
30+
[Dependency] private readonly IClyde _clyde = default!;
31+
[Dependency] private readonly ILogManager _logManager = default!;
32+
private ISawmill _sawmill = default!;
3033

3134
private DreamViewOverlay? _dreamViewOverlay;
3235
private ContextMenuPopup _contextMenu = default!;
@@ -41,6 +44,7 @@ private sealed class EntityClickInformation(ClientObjectReference atom, ScreenCo
4144

4245
public override void Initialize() {
4346
UpdatesOutsidePrediction = true;
47+
_sawmill = _logManager.GetSawmill("opendream.mouseinput");
4448

4549
_contextMenu = new ContextMenuPopup();
4650
_userInterfaceManager.ModalRoot.AddChild(_contextMenu);
@@ -54,8 +58,11 @@ public override void Update(float frameTime) {
5458
var currentMousePos = _inputManager.MouseScreenPosition.Position;
5559
var distance = (currentMousePos - _selectedEntity.InitialMousePos.Position).Length();
5660

57-
if (distance > 3f)
61+
if (distance > 3f) {
5862
_selectedEntity.IsDrag = true;
63+
if (_dreamInterfaceManager.DefaultMap is { } map)
64+
UpdateMouseCursor(map.Viewport, _selectedEntity.Atom);
65+
}
5966
}
6067
}
6168

@@ -79,13 +86,15 @@ public void HandleStatClick(string atomRef, bool isRight, bool isMiddle) {
7986
}
8087

8188
public void HandleAtomMouseEntered(ScalingViewport viewport, Vector2 relativePos, ClientObjectReference atomRef, Vector2i iconPos) {
89+
UpdateMouseCursor(viewport, atomRef);
8290
if (!HasMouseEventEnabled(atomRef, AtomMouseEvents.Enter))
8391
return;
8492

8593
RaiseNetworkEvent(new MouseEnteredEvent(atomRef, CreateClickParams(viewport, relativePos, iconPos)));
8694
}
8795

8896
public void HandleAtomMouseExited(ScalingViewport viewport, ClientObjectReference atomRef) {
97+
UpdateMouseCursor(viewport, null);
8998
if (!HasMouseEventEnabled(atomRef, AtomMouseEvents.Exit))
9099
return;
91100

@@ -178,21 +187,71 @@ private bool OnPress(ScalingViewport viewport, GUIBoundKeyEventArgs args, Contro
178187
}
179188

180189
private bool OnRelease(ScalingViewport viewport, GUIBoundKeyEventArgs args) {
181-
if (_selectedEntity == null)
190+
if (_selectedEntity == null) {
191+
UpdateMouseCursor(viewport, null);
182192
return false;
193+
}
183194

195+
var overAtom = GetAtomUnderMouse(viewport, args.RelativePixelPosition, args.PointerLocation);
184196
if (!_selectedEntity.IsDrag) {
185197
RaiseNetworkEvent(new AtomClickedEvent(_selectedEntity.Atom, _selectedEntity.ClickParams));
186198
} else {
187-
var overAtom = GetAtomUnderMouse(viewport, args.RelativePixelPosition, args.PointerLocation);
188-
189199
RaiseNetworkEvent(new AtomDraggedEvent(_selectedEntity.Atom, overAtom?.Atom, _selectedEntity.ClickParams));
190200
}
191201

192202
_selectedEntity = null;
203+
UpdateMouseCursor(viewport, overAtom?.Atom);
193204
return true;
194205
}
195206

207+
private void UpdateMouseCursor(ScalingViewport viewport, ClientObjectReference? mouseOver) {
208+
var isDragging = _selectedEntity?.IsDrag ?? false;
209+
if (!mouseOver.HasValue || !_appearanceSystem.TryGetAppearance(mouseOver.Value, out var mouseOverAppearance)) {
210+
if (!isDragging)
211+
SetCursorFromDefine(1, _dreamInterfaceManager.Cursors.BaseCursor, viewport);
212+
return;
213+
}
214+
215+
if (isDragging) {
216+
if (!_appearanceSystem.TryGetAppearance(_selectedEntity!.Atom, out var draggingAppearance)) {
217+
SetCursorFromDefine(1, _dreamInterfaceManager.Cursors.DragCursor, viewport);
218+
return;
219+
}
220+
221+
var define = mouseOverAppearance.MouseDropZone
222+
? mouseOverAppearance.MouseDropPointer
223+
: draggingAppearance.MouseDragPointer;
224+
var cursor = mouseOverAppearance.MouseDropZone
225+
? _dreamInterfaceManager.Cursors.DropCursor
226+
: _dreamInterfaceManager.Cursors.DragCursor;
227+
SetCursorFromDefine(define, cursor, viewport);
228+
} else {
229+
SetCursorFromDefine(mouseOverAppearance.MouseOverPointer, _dreamInterfaceManager.Cursors.OverCursor, viewport);
230+
}
231+
}
232+
233+
private void SetCursorFromDefine(int define, ICursor? activeCursor, ScalingViewport viewport) {
234+
_sawmill.Verbose($"SetCursor {define} {activeCursor}");
235+
236+
if (_dreamInterfaceManager.Cursors.AllStateSet) {
237+
viewport.CustomCursorShape = _dreamInterfaceManager.Cursors.BaseCursor;
238+
} else {
239+
viewport.CustomCursorShape = define switch {
240+
0 => _dreamInterfaceManager.Cursors.BaseCursor, //MOUSE_INACTIVE_POINTER
241+
1 => activeCursor, //MOUSE_ACTIVE_POINTER
242+
//skipping 2 is intentional, it's what byond does
243+
3 => _clyde.GetStandardCursor(StandardCursorShape.Crosshair), //MOUSE_DRAG_POINTER
244+
4 => _clyde.GetStandardCursor(StandardCursorShape.Hand), //MOUSE_DROP_POINTER
245+
5 => _clyde.GetStandardCursor(StandardCursorShape.Arrow), //MOUSE_ARROW_POINTER
246+
6 => _clyde.GetStandardCursor(StandardCursorShape.Crosshair), //MOUSE_CROSSHAIRS_POINTER
247+
7 => _clyde.GetStandardCursor(StandardCursorShape.Hand), //MOUSE_HAND_POINTER
248+
_ => null
249+
};
250+
}
251+
252+
_clyde.SetCursor(viewport.CustomCursorShape);
253+
}
254+
196255
private ClickParams CreateClickParams(ScalingViewport viewport, GUIBoundKeyEventArgs args, Vector2i iconPos) {
197256
bool right = args.Function == EngineKeyFunctions.UIRightClick;
198257
bool middle = args.Function == OpenDreamKeyFunctions.MouseMiddle;
@@ -202,7 +261,7 @@ private ClickParams CreateClickParams(ScalingViewport viewport, GUIBoundKeyEvent
202261
UIBox2i viewportBox = viewport.GetDrawBox();
203262
Vector2 screenLocPos = (args.RelativePixelPosition - viewportBox.TopLeft) / viewportBox.Size * viewport.ViewportSize;
204263
float screenLocY = viewport.ViewportSize.Y - screenLocPos.Y; // Flip the Y
205-
ScreenLocation screenLoc = new ScreenLocation((int) screenLocPos.X, (int) screenLocY, 32); // TODO: icon_size other than 32
264+
ScreenLocation screenLoc = new ScreenLocation((int)screenLocPos.X, (int)screenLocY, 32); // TODO: icon_size other than 32
206265

207266
// TODO: Take icon transformations into account for iconPos
208267
return new(screenLoc, right, middle, shift, ctrl, alt, iconPos.X, iconPos.Y);

OpenDreamClient/Interface/Controls/ControlMap.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,14 @@ public override bool TryGetProperty(string property, [NotNullWhen(true)] out IDM
140140
private void UpdateAtomUnderMouse(ClientObjectReference? atom, Vector2 relativePos, Vector2i iconPos) {
141141
if (!_atomUnderMouse.Equals(atom)) {
142142
_entitySystemManager.Resolve(ref _appearanceSystem);
143-
144143
var name = (atom != null) ? _appearanceSystem.GetName(atom.Value) : string.Empty;
145144
Window?.SetStatus(name);
146145

147146
if (_atomUnderMouse != null)
148147
_mouseInput?.HandleAtomMouseExited(Viewport, _atomUnderMouse.Value);
149-
if (atom != null)
148+
if (atom != null) {
150149
_mouseInput?.HandleAtomMouseEntered(Viewport, relativePos, atom.Value, iconPos);
150+
}
151151
} else if (atom.HasValue) {
152152
_mouseInput?.HandleAtomMouseMove(Viewport, relativePos, atom.Value, iconPos);
153153
}

OpenDreamClient/Interface/DreamInterfaceManager.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ internal sealed class DreamInterfaceManager : IDreamInterfaceManager {
5858
public Dictionary<string, InterfaceMenu> Menus { get; } = new();
5959
public Dictionary<string, InterfaceMacroSet> MacroSets { get; } = new();
6060
private Dictionary<WindowId, ControlWindow> ClydeWindowIdToControl { get; } = new();
61+
public CursorHolder Cursors { get; private set; } = default!;
6162

6263
public ViewRange View {
6364
get => _view;
@@ -110,6 +111,8 @@ public void Initialize() {
110111
BaseKey = Keyboard.Key.MouseMiddle
111112
});
112113

114+
Cursors = new(_clyde);
115+
113116
_netManager.RegisterNetMessage<MsgUpdateStatPanels>(RxUpdateStatPanels);
114117
_netManager.RegisterNetMessage<MsgSelectStatPanel>(RxSelectStatPanel);
115118
_netManager.RegisterNetMessage<MsgOutput>(RxOutput);
@@ -326,6 +329,14 @@ private void RxUpdateClientInfo(MsgUpdateClientInfo msg) {
326329
IconSize = msg.IconSize;
327330
View = msg.View;
328331
ShowPopupMenus = msg.ShowPopupMenus;
332+
if (msg.CursorResource != 0)
333+
_dreamResource.LoadResourceAsync<DMIResource>(msg.CursorResource, resource => {
334+
//TODO should trigger a cursor update immediately
335+
Cursors = new(_clyde, resource);
336+
});
337+
else {
338+
Cursors = new(_clyde); //reset to default
339+
}
329340
}
330341

331342
private void ShowPrompt(PromptWindow prompt) {
@@ -1024,6 +1035,40 @@ private void OnPromptFinished(int promptId, DreamValueType responseType, object?
10241035
}
10251036
}
10261037

1038+
public sealed class CursorHolder(IClyde clyde) {
1039+
public readonly ICursor? BaseCursor;
1040+
public readonly ICursor? DragCursor = clyde.GetStandardCursor(StandardCursorShape.Crosshair);
1041+
public readonly ICursor? OverCursor;
1042+
public readonly ICursor? DropCursor = clyde.GetStandardCursor(StandardCursorShape.Hand);
1043+
public readonly bool AllStateSet;
1044+
1045+
public CursorHolder(IClyde clyde, DMIResource resource) : this(clyde) {
1046+
var allState = resource.GetStateAsImage("all", AtomDirection.South);
1047+
1048+
if (allState is not null) { //all overrides all possible states
1049+
BaseCursor = clyde.CreateCursor(allState, new(32, 32));
1050+
DragCursor = BaseCursor;
1051+
DropCursor = BaseCursor;
1052+
OverCursor = BaseCursor;
1053+
AllStateSet = true;
1054+
} else {
1055+
var baseState = resource.GetStateAsImage("", AtomDirection.South);
1056+
var overState = resource.GetStateAsImage("over", AtomDirection.South);
1057+
var dragState = resource.GetStateAsImage("drag", AtomDirection.South);
1058+
var dropState = resource.GetStateAsImage("drop", AtomDirection.South);
1059+
1060+
if (baseState is not null)
1061+
BaseCursor = clyde.CreateCursor(baseState, new(32, 32));
1062+
if (overState is not null)
1063+
OverCursor = clyde.CreateCursor(overState, new(32, 32));
1064+
if (dragState is not null)
1065+
DragCursor = clyde.CreateCursor(dragState, new(32, 32));
1066+
if (dropState is not null)
1067+
DropCursor = clyde.CreateCursor(dropState, new(32, 32));
1068+
}
1069+
}
1070+
}
1071+
10271072
public interface IDreamInterfaceManager {
10281073
Dictionary<string, ControlWindow> Windows { get; }
10291074
Dictionary<string, InterfaceMenu> Menus { get; }
@@ -1035,6 +1080,7 @@ public interface IDreamInterfaceManager {
10351080
public ViewRange View { get; }
10361081
public bool ShowPopupMenus { get; }
10371082
public int IconSize { get; }
1083+
public CursorHolder Cursors { get; }
10381084

10391085
void Initialize();
10401086
void FrameUpdate(FrameEventArgs frameEventArgs);

OpenDreamClient/Interface/DummyDreamInterfaceManager.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ public sealed class DummyDreamInterfaceManager : IDreamInterfaceManager {
2020
public ViewRange View => new(5);
2121
public bool ShowPopupMenus => true;
2222
public int IconSize => 32;
23+
public CursorHolder Cursors => null!;
2324

2425
[Dependency] private readonly IClientNetManager _netManager = default!;
2526

2627
public void Initialize() {
27-
_netManager.RegisterNetMessage<MsgLoadInterface>((_) => _netManager.ClientSendMessage(new MsgAckLoadInterface()));
28+
_netManager.RegisterNetMessage<MsgLoadInterface>(_ => _netManager.ClientSendMessage(new MsgAckLoadInterface()));
2829
}
2930

3031
public void FrameUpdate(FrameEventArgs frameEventArgs) {

OpenDreamClient/Resources/ResourceTypes/DMIResource.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Robust.Client.Graphics;
55
using SixLabors.ImageSharp;
66
using SixLabors.ImageSharp.PixelFormats;
7+
using SixLabors.ImageSharp.Processing;
78

89
namespace OpenDreamClient.Resources.ResourceTypes;
910

@@ -50,6 +51,23 @@ private void ProcessDMIData() {
5051
return _states[stateName];
5152
}
5253

54+
public Image<Rgba32>? GetStateAsImage(string? stateName, AtomDirection dir) {
55+
using Stream dmiStream = new MemoryStream(Data);
56+
DMIParser.ParsedDMIDescription description = DMIParser.ParseDMI(dmiStream);
57+
58+
dmiStream.Seek(0, SeekOrigin.Begin);
59+
60+
Image<Rgba32> image = Image.Load<Rgba32>(dmiStream);
61+
if (!(description.GetStateOrDefault(stateName)?.Directions.TryGetValue(dir, out var state) ?? false))
62+
return null;
63+
64+
var result = image.Clone(clone => {
65+
clone.Resize(new Size(description.Width, description.Height));
66+
clone.Crop(new Rectangle(state[0].X, state[0].Y, state[0].X + description.Width, state[0].Y + description.Height));
67+
});
68+
return result;
69+
}
70+
5371
public struct State {
5472
public Dictionary<AtomDirection, AtlasTexture[]> Frames;
5573

OpenDreamRuntime/AtomManager.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ public bool IsValidAppearanceVar(string name) {
265265
case "maptext_height":
266266
case "maptext_x":
267267
case "maptext_y":
268+
case "mouse_drag_pointer":
269+
case "mouse_drop_pointer":
270+
case "mouse_drop_zone":
271+
case "mouse_over_pointer":
268272
return true;
269273

270274
// Get/SetAppearanceVar doesn't handle filters right now
@@ -422,6 +426,18 @@ public void SetAppearanceVar(MutableAppearance appearance, string varName, Dream
422426
case "maptext_y":
423427
value.TryGetValueAsInteger(out appearance.MaptextOffset.Y);
424428
break;
429+
case "mouse_drag_pointer":
430+
value.TryGetValueAsInteger(out appearance.MouseDragPointer);
431+
break;
432+
case "mouse_drop_pointer":
433+
value.TryGetValueAsInteger(out appearance.MouseDropPointer);
434+
break;
435+
case "mouse_drop_zone":
436+
appearance.MouseDropZone = value.IsTruthy();
437+
break;
438+
case "mouse_over_pointer":
439+
value.TryGetValueAsInteger(out appearance.MouseOverPointer);
440+
break;
425441
case "appearance":
426442
throw new Exception("Cannot assign the appearance var on an appearance");
427443

@@ -530,6 +546,14 @@ public DreamValue GetAppearanceVar(ImmutableAppearance appearance, string varNam
530546
return new(appearance.MaptextOffset.X);
531547
case "maptext_y":
532548
return new(appearance.MaptextOffset.Y);
549+
case "mouse_drag_pointer":
550+
return new(appearance.MouseDragPointer);
551+
case "mouse_drop_pointer":
552+
return new(appearance.MouseDropPointer);
553+
case "mouse_drop_zone":
554+
return appearance.MouseDropZone ? DreamValue.True : DreamValue.False;
555+
case "mouse_over_pointer":
556+
return new(appearance.MouseOverPointer);
533557
case "appearance":
534558
MutableAppearance appearanceCopy = appearance.ToMutable(); // Return a copy
535559
return new(appearanceCopy);
@@ -730,6 +754,10 @@ public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def)
730754
def.TryGetVariable("maptext_height", out var maptextHeightVar);
731755
def.TryGetVariable("maptext_x", out var maptextXVar);
732756
def.TryGetVariable("maptext_y", out var maptextYVar);
757+
def.TryGetVariable("mouse_over_pointer", out var mouseOverPointer);
758+
def.TryGetVariable("mouse_drag_pointer", out var mouseDragPointer);
759+
def.TryGetVariable("mouse_drop_pointer", out var mouseDropPointer);
760+
def.TryGetVariable("mouse_drop_zone", out var mouseDropZone);
733761

734762
appearance = MutableAppearance.Get();
735763
SetAppearanceVar(appearance, "name", nameVar);
@@ -756,6 +784,10 @@ public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def)
756784
SetAppearanceVar(appearance, "maptext_height", maptextHeightVar);
757785
SetAppearanceVar(appearance, "maptext_x", maptextXVar);
758786
SetAppearanceVar(appearance, "maptext_y", maptextYVar);
787+
SetAppearanceVar(appearance, "mouse_over_pointer", mouseOverPointer);
788+
SetAppearanceVar(appearance, "mouse_drag_pointer", mouseDragPointer);
789+
SetAppearanceVar(appearance, "mouse_drop_pointer", mouseDropPointer);
790+
SetAppearanceVar(appearance, "mouse_drop_zone", mouseDropZone);
759791

760792
if (def.TryGetVariable("transform", out var transformVar) && transformVar.TryGetValueAsDreamObject<DreamObjectMatrix>(out var transformMatrix)) {
761793
appearance.Transform = DreamObjectMatrix.MatrixToTransformFloatArray(transformMatrix);

0 commit comments

Comments
 (0)