From a143b685566527dd464690231e831ee88e1691f0 Mon Sep 17 00:00:00 2001 From: Lucy Date: Mon, 1 Jan 2024 23:22:55 -0500 Subject: [PATCH] Implement color inputting (#1595) * Implement color inputting * Update OpenDreamRuntime/DreamConnection.cs Co-authored-by: wixoa * bwah --------- Co-authored-by: wixoa --- .../Interface/DreamInterfaceManager.cs | 60 +++--- .../Interface/Prompts/ColorPrompt.cs | 175 ++++++++++++++++++ OpenDreamRuntime/DreamConnection.cs | 8 +- .../Network/Messages/MsgPromptResponse.cs | 17 +- .../Textures/Interface/Nano/slider_fill.svg | 92 +++++++++ .../Interface/Nano/slider_fill.svg.96dpi.png | Bin 0 -> 267 bytes .../Interface/Nano/slider_grabber.svg | 89 +++++++++ .../Nano/slider_grabber.svg.96dpi.png | Bin 0 -> 285 bytes .../Interface/Nano/slider_outline.svg | 91 +++++++++ .../Nano/slider_outline.svg.96dpi.png | Bin 0 -> 284 bytes 10 files changed, 496 insertions(+), 36 deletions(-) create mode 100644 OpenDreamClient/Interface/Prompts/ColorPrompt.cs create mode 100644 Resources/Textures/Interface/Nano/slider_fill.svg create mode 100644 Resources/Textures/Interface/Nano/slider_fill.svg.96dpi.png create mode 100644 Resources/Textures/Interface/Nano/slider_grabber.svg create mode 100644 Resources/Textures/Interface/Nano/slider_grabber.svg.96dpi.png create mode 100644 Resources/Textures/Interface/Nano/slider_outline.svg create mode 100644 Resources/Textures/Interface/Nano/slider_outline.svg.96dpi.png diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index e82df03b06..d39e65a149 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -215,6 +215,8 @@ void OnPromptClose(DMValueType responseType, object? response) { prompt = new NumberPrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); } else if ((pPrompt.Types & DMValueType.Message) == DMValueType.Message) { prompt = new MessagePrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); + } else if ((pPrompt.Types & DMValueType.Color) == DMValueType.Color) { + prompt = new ColorPrompt(pPrompt.Title, pPrompt.Message, pPrompt.DefaultValue, canCancel, OnPromptClose); } if (prompt != null) { @@ -368,7 +370,7 @@ public void FrameUpdate(FrameEventArgs frameEventArgs) { } else if (_popupWindows.TryGetValue(windowId, out var popup)) { window = popup.WindowElement; } else if (Menus.TryGetValue(windowId, out var menu)) { - if(menu.MenuElements.TryGetValue(elementId, out var menuElement)) + if (menu.MenuElements.TryGetValue(elementId, out var menuElement)) return menuElement; } @@ -426,45 +428,45 @@ public void SaveScreenshot(bool openDialog) { }); } - public void RunCommand(string command){ + public void RunCommand(string command) { switch (command) { - case string x when x.StartsWith(".quit"): - IoCManager.Resolve().ClientDisconnect(".quit used"); - break; + case string x when x.StartsWith(".quit"): + IoCManager.Resolve().ClientDisconnect(".quit used"); + break; - case string x when x.StartsWith(".screenshot"): - string[] split = command.Split(" "); - SaveScreenshot(split.Length == 1 || split[1] != "auto"); - break; + case string x when x.StartsWith(".screenshot"): + string[] split = command.Split(" "); + SaveScreenshot(split.Length == 1 || split[1] != "auto"); + break; - case string x when x.StartsWith(".configure"): - _sawmill.Warning(".configure command is not implemented"); - break; + case string x when x.StartsWith(".configure"): + _sawmill.Warning(".configure command is not implemented"); + break; - case string x when x.StartsWith(".winset"): - // Everything after .winset, excluding the space and quotes - string winsetParams = command.Substring(7); //clip .winset - winsetParams = winsetParams.Trim(); //clip space - winsetParams = winsetParams.Trim('\"'); //clip quotes + case string x when x.StartsWith(".winset"): + // Everything after .winset, excluding the space and quotes + string winsetParams = command.Substring(7); //clip .winset + winsetParams = winsetParams.Trim(); //clip space + winsetParams = winsetParams.Trim('\"'); //clip quotes - WinSet(null, winsetParams); - break; + WinSet(null, winsetParams); + break; - default: { - // Send the entire command to the server. - // It has more info about argument types so it can parse it better than we can. - _netManager.ClientSendMessage(new MsgCommand(){Command = command}); - break; - } + default: { + // Send the entire command to the server. + // It has more info about argument types so it can parse it better than we can. + _netManager.ClientSendMessage(new MsgCommand() { Command = command }); + break; } + } } public void StartRepeatingCommand(string command) { - _netManager.ClientSendMessage(new MsgCommandRepeatStart(){Command = command}); + _netManager.ClientSendMessage(new MsgCommandRepeatStart() { Command = command }); } public void StopRepeatingCommand(string command) { - _netManager.ClientSendMessage(new MsgCommandRepeatStop(){Command = command}); + _netManager.ClientSendMessage(new MsgCommandRepeatStop() { Command = command }); } public void WinSet(string? controlId, string winsetParams) { @@ -615,7 +617,7 @@ public void WinClone(string controlId, string cloneId) { // that name already, we will create a new control of that type from scratch. if (elementDescriptor == null) { switch (controlId) { - case "window" : + case "window": elementDescriptor = new WindowDescriptor(cloneId); break; case "menu": @@ -636,7 +638,7 @@ public void WinClone(string controlId, string cloneId) { } LoadDescriptor(elementDescriptor); - if(elementDescriptor is WindowDescriptor && Windows.TryGetValue(cloneId, out var window)){ + if (elementDescriptor is WindowDescriptor && Windows.TryGetValue(cloneId, out var window)) { window.CreateChildControls(); } } diff --git a/OpenDreamClient/Interface/Prompts/ColorPrompt.cs b/OpenDreamClient/Interface/Prompts/ColorPrompt.cs new file mode 100644 index 0000000000..81dc53e04c --- /dev/null +++ b/OpenDreamClient/Interface/Prompts/ColorPrompt.cs @@ -0,0 +1,175 @@ +using System.Linq; +using Linguini.Bundle.Errors; +using OpenDreamShared.Dream; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.Stylesheets; + +namespace OpenDreamClient.Interface.Prompts; + +internal sealed class ColorPrompt : InputWindow { + private readonly BoxContainer _baseControl; + private readonly ColorSelectorSliders _colorSelector; + private readonly LineEdit _hexColor; + private readonly Button _preview; + private readonly Color _originalColor; + + public ColorPrompt(string title, string message, string defaultValue, bool canCancel, + Action? onClose, bool alpha = false) : base(title, message, canCancel, onClose) { + _originalColor = Color.FromHex(defaultValue, Color.White); + _colorSelector = new() { + Color = _originalColor, + VerticalAlignment = VAlignment.Top, + Stylesheet = ColorPromptStylesheet.Make(), + IsAlphaVisible = alpha, + OnColorChanged = ColorSelectorSliders_OnColorChanged + }; + var defaultHex = _colorSelector.IsAlphaVisible ? _originalColor.ToHex() : _originalColor.ToHexNoAlpha(); + _hexColor = new LineEdit { + Text = defaultHex, + HorizontalExpand = true, + PlaceHolder = _colorSelector.IsAlphaVisible ? "#RRGGBBAA" : "#RRGGBB", + IsValid = (string text) => { + text = text.Trim().TrimStart('#'); + return text.Length <= (_colorSelector.IsAlphaVisible ? 8 : 6) && text.All(char.IsAsciiHexDigit); + } + }; + _hexColor.OnFocusExit += LineEdit_OnFinishInput; + _hexColor.OnTextEntered += LineEdit_OnFinishInput; + + _preview = new Button { + SetSize = new Vector2(32, 32), + Modulate = _originalColor, + MouseFilter = MouseFilterMode.Ignore, + }; + + _baseControl = new BoxContainer { + Orientation = BoxContainer.LayoutOrientation.Vertical, + Children = { + _colorSelector, + new BoxContainer { + Orientation = BoxContainer.LayoutOrientation.Horizontal, + HorizontalExpand = true, + SeparationOverride = 8, + Children = { _preview, _hexColor } + } + } + }; + + SetPromptControl(_baseControl, grabKeyboard: false); + } + + private void ColorSelectorSliders_OnColorChanged(Color color) { + _hexColor.Text = _colorSelector.IsAlphaVisible ? color.ToHex() : color.ToHexNoAlpha(); + _preview.Modulate = color; + } + + private void LineEdit_OnFinishInput(LineEdit.LineEditEventArgs args) { + var text = args.Text.Trim(); + if (!text.StartsWith('#')) text = '#' + text; + var newColor = Color.TryFromHex(text); + if (newColor.HasValue) { + _colorSelector.Color = newColor.Value; + } + } + + protected override void OkButtonClicked() { + FinishPrompt(DMValueType.Color, _colorSelector.Color); + } +} + +// WHY IS ColorSelectorSliders A PART OF ROBUST, BUT THE STYLESHEET IT USES IS IN SS14?! +internal static class ColorPromptStylesheet { + private static readonly Color PanelDark = Color.FromHex("#1E1E22"); + private const string StyleClassSliderRed = "Red"; + private const string StyleClassSliderGreen = "Green"; + private const string StyleClassSliderBlue = "Blue"; + private const string StyleClassSliderWhite = "White"; + + private static StyleBoxTexture MakeSliderFill(Color color) { + return new() { + Texture = IoCManager.Resolve().GetResource("/Textures/Interface/Nano/slider_fill.svg.96dpi.png").Texture, + Modulate = color, + PatchMarginLeft = 12, + PatchMarginRight = 12, + PatchMarginTop = 12, + PatchMarginBottom = 12, + }; + } + + private static StyleBoxTexture MakeSliderGrab() { + return new() { + Texture = IoCManager.Resolve().GetResource("/Textures/Interface/Nano/slider_grabber.svg.96dpi.png").Texture, + PatchMarginLeft = 12, + PatchMarginRight = 12, + PatchMarginTop = 12, + PatchMarginBottom = 12, + }; + } + + private static StyleBoxTexture MakeSliderOutline(Color color) { + return new() { + Texture = IoCManager.Resolve().GetResource("/Textures/Interface/Nano/slider_outline.svg.96dpi.png").Texture, + Modulate = color, + PatchMarginLeft = 12, + PatchMarginRight = 12, + PatchMarginTop = 12, + PatchMarginBottom = 12, + }; + } + + + public static Stylesheet Make() { + var sliderFillBox = MakeSliderFill(Color.FromHex("#3E6C45")); + var sliderBackBox = MakeSliderOutline(PanelDark); + var sliderForeBox = MakeSliderOutline(Color.FromHex("#494949")); + var sliderGrabBox = MakeSliderGrab(); + + var sliderFillGreen = new StyleBoxTexture(sliderFillBox) { Modulate = Color.LimeGreen }; + var sliderFillRed = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Red }; + var sliderFillBlue = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Blue }; + var sliderFillWhite = new StyleBoxTexture(sliderFillBox) { Modulate = Color.White }; + + var styles = new DefaultStylesheet(IoCManager.Resolve(), IoCManager.Resolve()).Stylesheet.Rules.ToList(); + var newStyles = new StyleRule[] { + // Slider + new StyleRule(SelectorElement.Type(typeof(Slider)), new [] + { + new StyleProperty(Slider.StylePropertyBackground, sliderBackBox), + new StyleProperty(Slider.StylePropertyForeground, sliderForeBox), + new StyleProperty(Slider.StylePropertyGrabber, sliderGrabBox), + new StyleProperty(Slider.StylePropertyFill, sliderFillBox), + }), + + new StyleRule(SelectorElement.Type(typeof(ColorableSlider)), new [] + { + new StyleProperty(ColorableSlider.StylePropertyFillWhite, sliderFillWhite), + new StyleProperty(ColorableSlider.StylePropertyBackgroundWhite, sliderFillWhite), + }), + + new StyleRule(new SelectorElement(typeof(Slider), new []{StyleClassSliderRed}, null, null), new [] + { + new StyleProperty(Slider.StylePropertyFill, sliderFillRed), + }), + + new StyleRule(new SelectorElement(typeof(Slider), new []{StyleClassSliderGreen}, null, null), new [] + { + new StyleProperty(Slider.StylePropertyFill, sliderFillGreen), + }), + + new StyleRule(new SelectorElement(typeof(Slider), new []{StyleClassSliderBlue}, null, null), new [] + { + new StyleProperty(Slider.StylePropertyFill, sliderFillBlue), + }), + + new StyleRule(new SelectorElement(typeof(Slider), new []{StyleClassSliderWhite}, null, null), new [] + { + new StyleProperty(Slider.StylePropertyFill, sliderFillWhite), + }) + }; + styles.AddRange(newStyles); + return new Stylesheet(styles); + } +} diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs index 8dcf8a6271..6bc43f2579 100644 --- a/OpenDreamRuntime/DreamConnection.cs +++ b/OpenDreamRuntime/DreamConnection.cs @@ -30,7 +30,8 @@ public sealed class DreamConnection { [ViewVariables] public ICommonSession? Session { get; private set; } [ViewVariables] public DreamObjectClient? Client { get; private set; } - [ViewVariables] public DreamObjectMob? Mob { + [ViewVariables] + public DreamObjectMob? Mob { get => _mob; set { if (_mob != value) { @@ -234,7 +235,7 @@ public void AddStatPanelLine(string name, string value, string? atomRef) { if (_outputStatPanel == null || !_statPanels.ContainsKey(_outputStatPanel)) SetOutputStatPanel("Stats"); - _statPanels[_outputStatPanel].Add( (name, value, atomRef) ); + _statPanels[_outputStatPanel].Add((name, value, atomRef)); } public void HandleMsgSelectStatPanel(MsgSelectStatPanel message) { @@ -251,6 +252,7 @@ public void HandleMsgPromptResponse(MsgPromptResponse message) { DMValueType.Null => DreamValue.Null, DMValueType.Text or DMValueType.Message => new DreamValue((string)message.Value), DMValueType.Num => new DreamValue((float)message.Value), + DMValueType.Color => new DreamValue(((Color)message.Value).ToHexNoAlpha()), _ => throw new Exception("Invalid prompt response '" + message.Type + "'") }; @@ -372,7 +374,7 @@ public void HandleCommand(string fullCommand) { DMValueType argumentType = verb.ArgumentTypes[i]; if (argumentType == DMValueType.Text) { - arguments[i] = new(args[i+1]); + arguments[i] = new(args[i + 1]); } else { _sawmill.Error($"Parsing verb args of type {argumentType} is unimplemented; ignoring command ({fullCommand})"); return DreamValue.Null; diff --git a/OpenDreamShared/Network/Messages/MsgPromptResponse.cs b/OpenDreamShared/Network/Messages/MsgPromptResponse.cs index edd12eeb13..8acbe4f254 100644 --- a/OpenDreamShared/Network/Messages/MsgPromptResponse.cs +++ b/OpenDreamShared/Network/Messages/MsgPromptResponse.cs @@ -1,6 +1,7 @@ using System; using Lidgren.Network; using OpenDreamShared.Dream; +using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Serialization; @@ -14,12 +15,13 @@ public sealed class MsgPromptResponse : NetMessage { public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { PromptId = buffer.ReadVariableInt32(); - Type = (DMValueType) buffer.ReadUInt16(); + Type = (DMValueType)buffer.ReadUInt16(); Value = Type switch { DMValueType.Null => null, DMValueType.Text or DMValueType.Message => buffer.ReadString(), DMValueType.Num => buffer.ReadSingle(), + DMValueType.Color => new Color(buffer.ReadByte(), buffer.ReadByte(), buffer.ReadByte(), buffer.ReadByte()), _ => throw new ArgumentOutOfRangeException() }; } @@ -27,14 +29,21 @@ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { buffer.WriteVariableInt32(PromptId); - buffer.Write((ushort) Type); + buffer.Write((ushort)Type); switch (Type) { case DMValueType.Null: break; case DMValueType.Text or DMValueType.Message: - buffer.Write((string) Value!); + buffer.Write((string)Value!); break; case DMValueType.Num: - buffer.Write((float) Value!); + buffer.Write((float)Value!); + break; + case DMValueType.Color: + var color = (Color)Value!; + buffer.Write(color.RByte); + buffer.Write(color.GByte); + buffer.Write(color.BByte); + buffer.Write(color.AByte); break; default: throw new Exception("Invalid prompt response type '" + Type + "'"); } diff --git a/Resources/Textures/Interface/Nano/slider_fill.svg b/Resources/Textures/Interface/Nano/slider_fill.svg new file mode 100644 index 0000000000..615a923d6b --- /dev/null +++ b/Resources/Textures/Interface/Nano/slider_fill.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Resources/Textures/Interface/Nano/slider_fill.svg.96dpi.png b/Resources/Textures/Interface/Nano/slider_fill.svg.96dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..05a780078f98dcb1cdee34c2c165698c9e31d691 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^QXtI23?!pd0{;Rj=3*z$5DpHG+YkL80J)q69+AZi z417mGm~pB$pEOWVvcxr_Bsf2(yEr+qAXP8FD1G)j8<44@0X`wF zK>9xh@Hn5#0+e7Y3GxeO(9nJM^3{6h-)TURFi#i95Q*@qCpYpoIPf?JDl3X5H2OH6 z<(aAAyXQZL$i=yDCf&EKJ+RAT(whK7U*qX%d8I36EaMZsd}ZOI>{aqszQ-3I%AFK> g0R}Cjinsh`5VKE+y;)!^1hj#{)78&qol`;+0DJ9Y`Tzg` literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Nano/slider_grabber.svg b/Resources/Textures/Interface/Nano/slider_grabber.svg new file mode 100644 index 0000000000..e7075cfb26 --- /dev/null +++ b/Resources/Textures/Interface/Nano/slider_grabber.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Resources/Textures/Interface/Nano/slider_grabber.svg.96dpi.png b/Resources/Textures/Interface/Nano/slider_grabber.svg.96dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..557d1ca819b3fe792405d8491ad150e49c84211a GIT binary patch literal 285 zcmeAS@N?(olHy`uVBq!ia0vp^5(#!^6wV3&=1qFtD(&u(Y%UvUwK;t^!K4mIV0)GnCgibZkDSv;ZiO?CIhd zA`#wuVJB~c1CL8!zIaPZ%M=G;Gf{ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/Resources/Textures/Interface/Nano/slider_outline.svg.96dpi.png b/Resources/Textures/Interface/Nano/slider_outline.svg.96dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..de74c2dee04cecabc4b0e8afaa87d93d481a07bc GIT binary patch literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^QXtI11|(N{`J4k%EX7WqAsj$Z!;#Vf4nJ z@ErkR#;MwT(m+AU64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhK*rAWba4#v z=)8NykngYo53|F{e4Yt>f8FOkena-&Mag*7D+JdxF`^q&TwP7*$TWk$YsNftI(ZgzPQ$H6d?8Qr~b- Y`8L1({BIi(pd%SPUHx3vIVCg!0B=QNH2?qr literal 0 HcmV?d00001