Add quick interactions locally

This commit is contained in:
2025-03-31 04:02:39 +02:00
parent 5bb7a207f2
commit b7d4531ec8
134 changed files with 15494 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace QICrabUI
{
/// <summary>
/// A button
/// It's derived from CUITextBlock and has all its props
/// </summary>
public class CUIButton : CUITextBlock
{
[CUISerializable]
public GUISoundType ClickSound { get; set; } = GUISoundType.Select;
[CUISerializable] public Color DisabledColor { get; set; }
[CUISerializable] public Color InactiveColor { get; set; }
[CUISerializable] public Color MouseOverColor { get; set; }
[CUISerializable] public Color MousePressedColor { get; set; }
[CUISerializable] public bool AutoUpdateColor { get; set; } = true;
/// <summary>
/// Convenient prop to set all colors at once
/// </summary>
public Color MasterColor
{
set
{
InactiveColor = value.Multiply(0.7f);
MouseOverColor = value.Multiply(0.9f);
MousePressedColor = value;
DetermineColor();
}
}
public Color MasterColorOpaque
{
set
{
InactiveColor = new Color((int)(value.R * 0.7f), (int)(value.G * 0.7f), (int)(value.B * 0.7f), value.A);
MouseOverColor = new Color((int)(value.R * 0.9f), (int)(value.G * 0.9f), (int)(value.B * 0.9f), value.A);
MousePressedColor = value;
DetermineColor();
}
}
/// <summary>
/// BackgroundColor is used in base.Draw, but here it's calculated from colors above
/// So it's not a prop anymore, and i don't want to serialize it
/// </summary>
public new Color BackgroundColor
{
get => CUIProps.BackgroundColor.Value;
set => CUIProps.BackgroundColor.SetValue(value);
}
public void DetermineColor()
{
if (!AutoUpdateColor) return;
if (Disabled)
{
BackgroundColor = DisabledColor;
}
else
{
BackgroundColor = InactiveColor;
if (MouseOver) BackgroundColor = MouseOverColor;
if (MousePressed) BackgroundColor = MousePressedColor;
}
}
public override void Draw(SpriteBatch spriteBatch)
{
//DetermineColor();
base.Draw(spriteBatch);
}
public CUIButton() : base()
{
Text = "CUIButton";
ConsumeMouseClicks = true;
ConsumeDragAndDrop = true;
ConsumeSwipe = true;
OnMouseDown += (e) =>
{
if (!Disabled)
{
SoundPlayer.PlayUISound(ClickSound);
if (Command != null && Command != "")
{
DispatchUp(new CUICommand(Command));
}
}
};
OnMouseOff += (e) => DetermineColor();
OnMouseOn += (e) => DetermineColor();
OnStyleApplied += DetermineColor;
DetermineColor();
}
public CUIButton(string text) : this()
{
Text = text;
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Allows you to manipulate pixel data of its texture
/// </summary>
public class CUICanvas : CUIComponent, IDisposable
{
public Color[] Data;
public RenderTarget2D Texture;
/// <summary>
/// Size of the internal texture
/// Will automatically resize the texture and data array of set
/// </summary>
public virtual Point Size
{
get => new Point(Texture.Width, Texture.Height);
set
{
if (value.X == Texture?.Width && value.Y == Texture?.Height) return;
RenderTarget2D oldTexture = Texture;
Texture = new RenderTarget2D(GameMain.Instance.GraphicsDevice, value.X, value.Y);
Data = new Color[Texture.Width * Texture.Height];
BackgroundSprite = new CUISprite(Texture);
oldTexture?.Dispose();
}
}
public void Clear(Color? color = null)
{
Color cl = color ?? Color.Transparent;
for (int i = 0; i < Data.Length; i++)
{
Data[i] = cl;
}
SetData();
}
public Color GetPixel(int x, int y)
{
return Data[y * Texture.Width + x];
}
public void SetPixel(int x, int y, Color cl)
{
Data[y * Texture.Width + x] = cl;
}
/// <summary>
/// Call this method to transfer values from Data array into texture
/// </summary>
public void SetData()
{
Texture.SetData<Color>(Data);
}
/// <summary>
/// Uses renderFunc to render stuff directy onto Canvas.Texture
/// You can for example use GUI "Draw" methods with provided spriteBatch
/// </summary>
/// <param name="renderFunc"> Action<SpriteBatch> where you can draw whatever you want </param>
public void Render(Action<SpriteBatch> renderFunc)
{
GameMain.Instance.GraphicsDevice.SetRenderTarget(Texture);
//TODO save and restore scissor rect
spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable);
renderFunc(spriteBatch);
spriteBatch.End();
GameMain.Instance.GraphicsDevice.SetRenderTarget(null);
}
public SpriteBatch spriteBatch;
public CUICanvas(int x, int y) : base()
{
Size = new Point(x, y);
spriteBatch = new SpriteBatch(GameMain.Instance.GraphicsDevice);
}
public CUICanvas() : this(100, 100) { }
public override void CleanUp()
{
Texture?.Dispose();
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent : IDisposable
{
private void SetupAnimations()
{
Animations = new Indexer<string, CUIAnimation>(
(key) => animations.GetValueOrDefault(key),
(key, value) => AddAnimation(key, value)
);
}
private Dictionary<string, CUIAnimation> animations = new();
public Indexer<string, CUIAnimation> Animations;
public void AddAnimation(string name, CUIAnimation animation)
{
animation.Target = this;
animations[name] = animation;
}
public void BlockChildrenAnimations()
{
foreach (CUIComponent child in Children)
{
foreach (CUIAnimation animation in child.animations.Values)
{
animation.Stop();
animation.Blocked = true;
}
child.BlockChildrenAnimations();
}
}
}
}

View File

@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent
{
/// <summary>
/// Just a wrapper for CUIProps
/// idk how to separate them better
/// </summary>
//TODO this should be a dict, and cuiprop should have hash
public CUIComponentProps CUIProps { get; set; } = new();
public class CUIComponentProps
{
public CUIProp<int?> ZIndex = new CUIProp<int?>()
{
LayoutProp = true,
OnSet = (v, host) =>
{
foreach (var child in host.Children)
{
//HACK think, should i propagate null?
if (v.HasValue && !child.IgnoreParentZIndex)
{
child.ZIndex = v.Value + 1;
}
}
},
};
public CUIProp<bool> IgnoreEvents = new CUIProp<bool>()
{
OnSet = (v, host) =>
{
foreach (var child in host.Children)
{
if (!child.IgnoreParentEventIgnorance) child.IgnoreEvents = v;
}
},
};
public CUIProp<bool> Visible = new CUIProp<bool>()
{
Value = true,
OnSet = (v, host) =>
{
foreach (var child in host.Children)
{
if (!child.IgnoreParentVisibility) child.Visible = v;
}
},
};
public CUIProp<bool> Revealed = new CUIProp<bool>()
{
Value = true,
OnSet = (v, host) =>
{
// host.TreeChanged = true;
host.Visible = v;
host.IgnoreEvents = !v;
},
};
public CUIProp<CUIBool2> Ghost = new CUIProp<CUIBool2>()
{
LayoutProp = true,
AbsoluteProp = true,
};
public CUIProp<bool> CullChildren = new CUIProp<bool>()
{
OnSet = (v, host) =>
{
host.HideChildrenOutsideFrame = v;
},
};
public CUIProp<CUI3DOffset> ChildrenOffset = new CUIProp<CUI3DOffset>()
{
ChildProp = true,
Value = new CUI3DOffset(0, 0, 1), // uuuuuuuuu suka blyat!
Validate = (v, host) => host.ChildOffsetBounds.Check(v),
OnSet = (v, host) =>
{
foreach (var child in host.Children)
{
if (!child.Fixed) child.Scale = v.Z;
}
},
};
public CUIProp<bool> ResizeToSprite = new CUIProp<bool>()
{
LayoutProp = true,
OnSet = (v, host) =>
{
if (v)
{
host.Absolute = host.Absolute with
{
Width = host.BackgroundSprite.SourceRect.Width,
Height = host.BackgroundSprite.SourceRect.Height,
};
}
},
};
public CUIProp<CUIBool2> FillEmptySpace = new CUIProp<CUIBool2>()
{
LayoutProp = true,
};
public CUIProp<CUIBool2> FitContent = new CUIProp<CUIBool2>()
{
LayoutProp = true,
AbsoluteProp = true,
};
public CUIProp<CUINullRect> Absolute = new CUIProp<CUINullRect>()
{
LayoutProp = true,
AbsoluteProp = true,
};
public CUIProp<CUINullRect> AbsoluteMin = new CUIProp<CUINullRect>()
{
LayoutProp = true,
AbsoluteProp = true,
};
public CUIProp<CUINullRect> AbsoluteMax = new CUIProp<CUINullRect>()
{
LayoutProp = true,
AbsoluteProp = true,
};
public CUIProp<CUINullRect> Relative = new CUIProp<CUINullRect>()
{
LayoutProp = true,
};
public CUIProp<CUINullRect> RelativeMin = new CUIProp<CUINullRect>()
{
LayoutProp = true,
};
public CUIProp<CUINullRect> RelativeMax = new CUIProp<CUINullRect>()
{
LayoutProp = true,
};
public CUIProp<CUINullRect> CrossRelative = new CUIProp<CUINullRect>()
{
LayoutProp = true,
};
#region Graphic Props --------------------------------------------------------
#endregion
public CUIProp<PaletteOrder> Palette = new CUIProp<PaletteOrder>()
{
ShowInDebug = false,
OnSet = (v, host) =>
{
//TODO should this be called in deserialize?
CUIGlobalStyleResolver.OnComponentStyleChanged(host);
// foreach (var child in host.Children)
// {
// child.Palette = v;
// }
},
};
public CUIProp<CUISprite> BackgroundSprite = new CUIProp<CUISprite>()
{
Value = CUISprite.Default,
ShowInDebug = false,
Validate = (v, host) => v ?? CUISprite.Default,
OnSet = (v, host) =>
{
if (host.ResizeToSprite)
{
host.Absolute = host.Absolute with
{
Width = v.SourceRect.Width,
Height = v.SourceRect.Height,
};
}
if (host.IgnoreTransparent)
{
Rectangle bounds = host.BackgroundSprite.Texture.Bounds;
host.TextureData = new Color[bounds.Width * bounds.Height];
host.BackgroundSprite.Texture.GetData<Color>(host.TextureData);
}
},
};
public CUIProp<bool> IgnoreTransparent = new CUIProp<bool>()
{
OnSet = (v, host) =>
{
if (v)
{
Rectangle bounds = host.BackgroundSprite.Texture.Bounds;
host.TextureData = new Color[bounds.Width * bounds.Height];
host.BackgroundSprite.Texture.GetData<Color>(host.TextureData);
}
else
{
host.TextureData = null;
}
},
};
public CUIProp<Color> BackgroundColor = new CUIProp<Color>()
{
ShowInDebug = false,
OnSet = (v, host) =>
{
host.BackgroundVisible = v != Color.Transparent;
},
};
public CUIProp<Color> OutlineColor = new CUIProp<Color>()
{
ShowInDebug = false,
OnSet = (v, host) =>
{
host.OutlineVisible = v != Color.Transparent;
},
};
public CUIProp<Vector2> Padding = new CUIProp<Vector2>()
{
Value = new Vector2(2, 2),
DecorProp = true,
};
}
}
}

View File

@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent : IDisposable
{
/// <summary>
/// Global ID, unique for component
/// </summary>
public int ID { get; set; }
internal bool DebugHighlight { get; set; }
private CUIMainComponent mainComponent;
/// <summary>
/// Link to CUIMainComponent, passed to children
/// </summary>
public CUIMainComponent MainComponent
{
get => mainComponent;
set
{
mainComponent = value;
foreach (var child in Children) { child.MainComponent = value; }
}
}
internal int positionalZIndex;
internal int addedZIndex;
[Calculated] public bool Focused { get; set; }
/// <summary>
/// True when parent has HideChildrenOutsideFrame and child wanders beyond parents border
/// </summary>
[Calculated] internal bool CulledOut { get; set; }
/// <summary>
/// BackgroundColor != Color.Transparent
/// </summary>
protected bool BackgroundVisible { get; set; }
protected bool OutlineVisible { get; set; }
// This is for state clones, to protect them from style changes
internal bool Unreal { get; set; }
public bool MouseOver { get; set; }
public bool MousePressed { get; set; }
/// <summary>
/// This is used by text to prevent resizing beyond that
/// and works as AbsoluteMin
/// </summary>
[Calculated]
public CUINullVector2 ForcedMinSize
{
get => forsedSize;
set => SetForcedMinSize(value);
}
protected CUINullVector2 forsedSize; internal void SetForcedMinSize(CUINullVector2 value, [CallerMemberName] string memberName = "")
{
forsedSize = value;
CUIDebug.Capture(null, this, "SetForcedMinSize", memberName, "ForcedMinSize", ForcedMinSize.ToString());
OnPropChanged();//TODO this is the reason why lists with a lot of children lag
//OnSelfAndParentChanged();
OnAbsolutePropChanged();
}
/// <summary>
/// This is set by ChildrenOffset when zooming, and iirc consumed by text to adjust text scale
/// </summary>
[Calculated]
public float Scale
{
get => scale;
set => SetScale(value);
}
protected float scale = 1f; internal void SetScale(float value, [CallerMemberName] string memberName = "")
{
scale = value;
foreach (var child in Children) { child.Scale = value; }
// OnDecorPropChanged();
}
/// <summary>
/// Calculated Prop, Real + BorderThickness
/// </summary>
protected CUIRect BorderBox { get; set; }
protected CUIRect OutlineBox { get; set; }
internal Rectangle? ScissorRect { get; set; }
/// <summary>
/// Buffer for texture data, for IgnoreTransparent checks
/// </summary>
protected Color[] TextureData;
/// <summary>
/// Calculated prop, position on real screen in pixels
/// Should be fully calculated after CUIMainComponent.Update
/// </summary>
[Calculated]
public CUIRect Real
{
get => real;
set => SetReal(value);
}
private CUIRect real; internal void SetReal(CUIRect value, [CallerMemberName] string memberName = "")
{
//HACK idk if i need it
real = new CUIRect(
(float)Math.Round(value.Left),
(float)Math.Round(value.Top),
(float)Math.Round(value.Width),
(float)Math.Round(value.Height)
);
// real = value;
CUIDebug.Capture(null, this, "SetReal", memberName, "real", real.ToString());
BorderBox = real;
// BorderBox = new CUIRect(
// real.Left - BorderThickness,
// real.Top - BorderThickness,
// real.Width + 2 * BorderThickness,
// real.Height + 2 * BorderThickness
// );
OutlineBox = new CUIRect(
real.Left - OutlineThickness,
real.Top - OutlineThickness,
real.Width + 2 * OutlineThickness,
real.Height + 2 * OutlineThickness
);
if (HideChildrenOutsideFrame)
{
Rectangle SRect = real.Box;
// //HACK Remove these + 1
// Rectangle SRect = new Rectangle(
// (int)real.Left + 1,
// (int)real.Top + 1,
// (int)real.Width - 2,
// (int)real.Height - 2
// );
if (Parent?.ScissorRect != null)
{
ScissorRect = Rectangle.Intersect(Parent.ScissorRect.Value, SRect);
}
else
{
ScissorRect = SRect;
}
}
else ScissorRect = Parent?.ScissorRect;
}
}
}

View File

@@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public class CommandAttribute : System.Attribute { }
/// <summary>
/// Can be dispatched up the component tree to notify parent about something
/// add pass some event data without creating a hard link
/// </summary>
/// <param name="Name"></param>
public record CUICommand(string Name, object Data = null);
/// <summary>
/// Can be dispatched down the component tree to pass some data to the children
/// without creating a hard link
/// </summary>
public record CUIData(string Name, object Data = null);
public partial class CUIComponent
{
private void SetupCommands()
{
// This is actually expensive
//AddCommands();
OnTreeChanged += UpdateDataTargets;
}
/// <summary>
/// This command will be dispatched up when some component specific event happens
/// </summary>
[CUISerializable] public string Command { get; set; }
/// <summary>
/// this will be executed on any command
/// </summary>
public event Action<CUICommand> OnAnyCommand;
/// <summary>
/// Will be executed when receiving any data
/// </summary>
public event Action<CUIData> OnAnyData;
/// <summary>
/// Happens when appropriate data is received
/// </summary>
public event Action<Object> OnConsume;
/// <summary>
/// Will consume data with this name
/// </summary>
[CUISerializable] public string Consumes { get; set; }
private bool reflectCommands;
[CUISerializable]
public bool ReflectCommands
{
get => reflectCommands;
set
{
reflectCommands = value;
OnAnyCommand += (command) =>
{
foreach (CUIComponent child in Children)
{
child.DispatchDown(new CUIData(command.Name, command.Data));
}
};
}
}
private bool retranslateCommands;
[CUISerializable]
public bool RetranslateCommands
{
get => retranslateCommands;
set
{
retranslateCommands = value;
OnAnyCommand += (command) =>
{
Parent?.DispatchUp(command);
};
}
}
/// <summary>
/// Optimization to data flow
/// If not empty component will search for consumers of the data
/// and pass it directly to them instead of broadcasting it
/// </summary>
//[CUISerializable]
public ObservableCollection<string> Emits
{
get => emits;
set
{
emits = value;
emits.CollectionChanged += (o, e) => UpdateDataTargets();
UpdateDataTargets();
}
}
private ObservableCollection<string> emits = new();
private void UpdateDataTargets()
{
if (Emits.Count > 0)
{
DataTargets.Clear();
RunRecursiveOn(this, (c) =>
{
if (Emits.Contains(c.Consumes))
{
if (!DataTargets.ContainsKey(c.Consumes)) DataTargets[c.Consumes] = new();
DataTargets[c.Consumes].Add(c);
}
});
}
}
/// <summary>
/// Consumers of emmited data, updates on tree change
/// </summary>
public Dictionary<string, List<CUIComponent>> DataTargets = new();
/// <summary>
/// All commands
/// </summary>
public Dictionary<string, Action<object>> Commands { get; set; } = new();
/// <summary>
/// Manually adds command
/// </summary>
/// <param name="name"></param>
/// <param name="action"></param>
public void AddCommand(string name, Action<object> action) => Commands.Add(name, action);
public void RemoveCommand(string name) => Commands.Remove(name);
/// <summary>
/// Executed autpmatically on component creation
/// Methods ending in "Command" will be added as commands
/// </summary>
private void AddCommands()
{
foreach (MethodInfo mi in this.GetType().GetMethods())
{
if (Attribute.IsDefined(mi, typeof(CommandAttribute)))
{
try
{
string name = mi.Name;
if (name != "Command" && name.EndsWith("Command"))
{
name = name.Substring(0, name.Length - "Command".Length);
}
AddCommand(name, mi.CreateDelegate<Action<object>>(this));
}
catch (Exception e)
{
Info($"{e.Message}\nMethod: {this.GetType()}.{mi.Name}");
}
}
}
}
/// <summary>
/// Dispathes command up the component tree until someone consumes it
/// </summary>
/// <param name="command"></param>
public void DispatchUp(CUICommand command)
{
if (OnAnyCommand != null) OnAnyCommand?.Invoke(command);
else if (Commands.ContainsKey(command.Name)) Execute(command);
else Parent?.DispatchUp(command);
}
/// <summary>
/// Dispathes command down the component tree until someone consumes it
/// </summary>
public void DispatchDown(CUIData data)
{
if (Emits.Contains(data.Name))
{
if (DataTargets.ContainsKey(data.Name))
{
foreach (CUIComponent target in DataTargets[data.Name])
{
target.OnConsume?.Invoke(data.Data);
}
}
}
else
{
if (Consumes == data.Name) OnConsume?.Invoke(data.Data);
else if (OnAnyData != null) OnAnyData.Invoke(data);
else
{
foreach (CUIComponent child in Children) child.DispatchDown(data);
}
}
}
/// <summary>
/// Will execute action corresponding to this command
/// </summary>
/// <param name="commandName"></param>
public void Execute(CUICommand command)
{
Commands.GetValueOrDefault(command.Name)?.Invoke(command.Data);
}
}
}

View File

@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
#region Debug --------------------------------------------------------
/// <summary>
/// Mark component and its children for debug
/// Used in debug interface
/// </summary>
private bool debug; public bool Debug
{
get => debug;
set
{
debug = value;
//foreach (CUIComponent c in Children) { c.Debug = value; }
}
}
/// <summary>
/// For debug frame itself
/// </summary>
private bool ignoreDebug; public bool IgnoreDebug
{
get => ignoreDebug;
set
{
ignoreDebug = value;
foreach (CUIComponent c in Children) { c.IgnoreDebug = value; }
}
}
public void PrintTree(string offset = "")
{
CUI.Log($"{offset}{this}");
foreach (CUIComponent child in Children)
{
child.PrintTree(offset + "| ");
}
}
/// <summary>
/// Prints component and then message
/// </summary>
/// <param name="msg"></param>
/// <param name="source"></param>
/// <param name="lineNumber"></param>
public void Info(object msg, [CallerFilePath] string source = "", [CallerLineNumber] int lineNumber = 0)
{
var fi = new FileInfo(source);
CUI.Log($"{fi.Directory.Name}/{fi.Name}:{lineNumber}", Color.Yellow * 0.5f);
CUI.Log($"{this} {msg ?? "null"}", Color.Yellow);
}
#endregion
#region AKA --------------------------------------------------------
/// <summary>
/// Parent can memorize it's children by their names, AKA
/// </summary>
[CUISerializable] public string AKA { get; set; }
/// <summary>
/// All memorized components
/// </summary>
public Dictionary<string, CUIComponent> NamedComponents { get; set; } = new();
public CUIComponent Remember(CUIComponent c, string name)
{
NamedComponents[name] = c;
c.AKA = name;
return c;
}
/// <summary>
/// If it already has AKA
/// </summary>
public CUIComponent Remember(CUIComponent c)
{
if (c.AKA != null) NamedComponents[c.AKA] = c;
return c;
}
public CUIComponent Forget(string name)
{
if (name == null) return null;
CUIComponent c = NamedComponents.GetValueOrDefault(name);
NamedComponents.Remove(name);
return c;
}
/// <summary>
/// If it already has AKA
/// </summary>
public CUIComponent Forget(CUIComponent c)
{
if (c?.AKA != null) NamedComponents.Remove(c.AKA);
return c;
}
/// <summary>
/// You can access NamedComponents with this indexer
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public CUIComponent this[string name]
{
get => Get(name);
set
{
if (value.Parent != null) Remember(value, name);
else Append(value, name);
}
}
/// <summary>
/// Returns memorized component by name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public virtual CUIComponent Get(string name)
{
if (name == null) return null;
if (NamedComponents.ContainsKey(name)) return NamedComponents[name];
CUIComponent component = this;
string[] names = name.Split('.');
foreach (string n in names)
{
component = component.NamedComponents.GetValueOrDefault(n);
if (component == null)
{
CUI.Warning($"Failed to Get {name} from {this}, there's no {n}");
break;
}
}
return component;
}
public T Get<T>(string name) where T : CUIComponent => (T)Get(name);
#endregion
}
}

View File

@@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
#region Events --------------------------------------------------------
[CUISerializable] public bool ConsumeMouseClicks { get; set; }
[CUISerializable] public bool ConsumeDragAndDrop { get; set; }
[CUISerializable] public bool ConsumeSwipe { get; set; }
[CUISerializable] public bool ConsumeMouseScroll { get; set; }
//HACK no one will ever find it, hehehe
public void CascadeRefresh()
{
if (this is IRefreshable refreshable) refreshable.Refresh();
Children.ForEach(c => c.CascadeRefresh());
}
public event Action OnTreeChanged;
public event Action<double> OnUpdate;
public event Action<CUIInput> OnMouseLeave;
public event Action<CUIInput> OnMouseEnter;
public event Action<CUIInput> OnMouseDown;
public event Action<CUIInput> OnMouseUp;
public event Action<CUIInput> OnMouseMove;
public event Action<CUIInput> OnMouseOn;
public event Action<CUIInput> OnMouseOff;
public event Action<CUIInput> OnClick;
public event Action<CUIInput> OnDClick;
public event Action<CUIInput> OnScroll;
public event Action<float, float> OnDrag;
public event Action<float, float> OnSwipe;
public event Action<CUIInput> OnKeyDown;
public event Action<CUIInput> OnKeyUp;
public event Action<CUIInput> OnTextInput;
public event Action OnFocus;
public event Action OnFocusLost;
public Action<double> AddOnUpdate { set { OnUpdate += value; } }
public Action<CUIInput> AddOnMouseLeave { set { OnMouseLeave += value; } }
public Action<CUIInput> AddOnMouseEnter { set { OnMouseEnter += value; } }
public Action<CUIInput> AddOnMouseDown { set { OnMouseDown += value; } }
public Action<CUIInput> AddOnMouseUp { set { OnMouseUp += value; } }
public Action<CUIInput> AddOnMouseMove { set { OnMouseMove += value; } }
public Action<CUIInput> AddOnMouseOn { set { OnMouseOn += value; } }
public Action<CUIInput> AddOnMouseOff { set { OnMouseOff += value; } }
public Action<CUIInput> AddOnClick { set { OnClick += value; } }
public Action<CUIInput> AddOnDClick { set { OnDClick += value; } }
public Action<CUIInput> AddOnScroll { set { OnScroll += value; } }
public Action<float, float> AddOnDrag { set { OnDrag += value; } }
public Action<float, float> AddOnSwipe { set { OnSwipe += value; } }
public Action<CUIInput> AddOnKeyDown { set { OnKeyDown += value; } }
public Action<CUIInput> AddOnKeyUp { set { OnKeyUp += value; } }
public Action<CUIInput> AddOnTextInput { set { OnTextInput += value; } }
public Action AddOnFocus { set { OnFocus += value; } }
public Action AddOnFocusLost { set { OnFocusLost += value; } }
//TODO add more CUISpriteDrawModes
public virtual bool IsPointOnTransparentPixel(Vector2 point)
{
if (BackgroundSprite.DrawMode != CUISpriteDrawMode.Resize) return true;
//TODO hangle case where offset != sprite.origin
Vector2 RotationCenter = new Vector2(
BackgroundSprite.Offset.X * Real.Width,
BackgroundSprite.Offset.Y * Real.Height
);
Vector2 v = (point - Real.Position - RotationCenter).Rotate(-BackgroundSprite.Rotation) + RotationCenter;
float x = v.X / Real.Width;
float y = v.Y / Real.Height;
Rectangle bounds = BackgroundSprite.Texture.Bounds;
Rectangle SourceRect = BackgroundSprite.SourceRect;
int textureX = (int)Math.Round(SourceRect.X + x * SourceRect.Width);
int textureY = (int)Math.Round(SourceRect.Y + y * SourceRect.Height);
if (textureX < SourceRect.X || (SourceRect.X + SourceRect.Width - 1) < textureX) return true;
if (textureY < SourceRect.Y || (SourceRect.Y + SourceRect.Height - 1) < textureY) return true;
Color cl = TextureData[textureY * bounds.Width + textureX];
return cl.A == 0;
}
public virtual bool ShouldInvoke(CUIInput e)
{
if (IgnoreTransparent)
{
return !IsPointOnTransparentPixel(e.MousePosition);
}
return true;
}
internal void InvokeOnUpdate(double totalTime) => OnUpdate?.Invoke(totalTime);
internal void InvokeOnMouseLeave(CUIInput e) { OnMouseLeave?.Invoke(e); }
internal void InvokeOnMouseEnter(CUIInput e) { if (ShouldInvoke(e)) OnMouseEnter?.Invoke(e); }
internal void InvokeOnMouseDown(CUIInput e) { if (ShouldInvoke(e)) OnMouseDown?.Invoke(e); }
internal void InvokeOnMouseUp(CUIInput e) { if (ShouldInvoke(e)) OnMouseUp?.Invoke(e); }
internal void InvokeOnMouseMove(CUIInput e) { if (ShouldInvoke(e)) OnMouseMove?.Invoke(e); }
internal void InvokeOnMouseOn(CUIInput e) { if (ShouldInvoke(e)) OnMouseOn?.Invoke(e); }
internal void InvokeOnMouseOff(CUIInput e) { if (ShouldInvoke(e)) OnMouseOff?.Invoke(e); }
internal void InvokeOnClick(CUIInput e) { if (ShouldInvoke(e)) OnClick?.Invoke(e); }
internal void InvokeOnDClick(CUIInput e) { if (ShouldInvoke(e)) OnDClick?.Invoke(e); }
internal void InvokeOnScroll(CUIInput e) { if (ShouldInvoke(e)) OnScroll?.Invoke(e); }
internal void InvokeOnDrag(float x, float y) => OnDrag?.Invoke(x, y);
internal void InvokeOnSwipe(float x, float y) => OnSwipe?.Invoke(x, y);
internal void InvokeOnKeyDown(CUIInput e) { if (ShouldInvoke(e)) OnKeyDown?.Invoke(e); }
internal void InvokeOnKeyUp(CUIInput e) { if (ShouldInvoke(e)) OnKeyUp?.Invoke(e); }
internal void InvokeOnTextInput(CUIInput e) { if (ShouldInvoke(e)) OnTextInput?.Invoke(e); }
internal void InvokeOnFocus() => OnFocus?.Invoke();
internal void InvokeOnFocusLost() => OnFocusLost?.Invoke();
#endregion
#region Handles --------------------------------------------------------
internal CUIDragHandle DragHandle = new CUIDragHandle();
[CUISerializable]
public bool Draggable
{
get => DragHandle.Draggable;
set => DragHandle.Draggable = value;
}
//HACK Do i really need this?
internal CUIFocusHandle FocusHandle = new CUIFocusHandle();
[CUISerializable]
public bool Focusable
{
get => FocusHandle.Focusable;
set => FocusHandle.Focusable = value;
}
public CUIResizeHandle LeftResizeHandle = new CUIResizeHandle(new Vector2(0, 1), new CUIBool2(false, false));
public CUIResizeHandle RightResizeHandle = new CUIResizeHandle(new Vector2(1, 1), new CUIBool2(true, false));
public bool Resizible
{
get => ResizibleLeft || ResizibleRight;
set { ResizibleLeft = value; ResizibleRight = value; }
}
[CUISerializable]
public bool ResizibleLeft
{
get => LeftResizeHandle.Visible;
set => LeftResizeHandle.Visible = value;
}
[CUISerializable]
public bool ResizibleRight
{
get => RightResizeHandle.Visible;
set => RightResizeHandle.Visible = value;
}
[CUISerializable]
public CUIBool2 ResizeDirection
{
get => RightResizeHandle.Direction;
set
{
LeftResizeHandle.Direction = value;
RightResizeHandle.Direction = value;
}
}
internal CUISwipeHandle SwipeHandle = new CUISwipeHandle();
[CUISerializable]
public bool Swipeable
{
get => SwipeHandle.Swipeable;
set => SwipeHandle.Swipeable = value;
}
#endregion
}
}

View File

@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
/// <summary>
/// Used for text, should be in CUITextBlock really
/// </summary>
[CUISerializable]
public Vector2 Padding
{
get => CUIProps.Padding.Value;
set => CUIProps.Padding.SetValue(value);
}
/// <summary>
/// Should be one texture, not sprite sheet
/// Or there would be no way to wrap it
/// Top side will always point outwards
/// </summary>
[CUISerializable]
public CUISprite BorderSprite { get; set; } = CUISprite.Default;
/// <summary>
/// Container for Color and Thickness
/// Border is drawn inside the component and will eat space from content
/// If "by side" border prop != null then it'll take presidence
/// </summary>
[CUISerializable] public CUIBorder Border { get; set; } = new CUIBorder();
[CUISerializable] public CUIBorder TopBorder { get; set; }
[CUISerializable] public CUIBorder RigthBorder { get; set; }
[CUISerializable] public CUIBorder BottomBorder { get; set; }
[CUISerializable] public CUIBorder LeftBorder { get; set; }
[CUISerializable]
public float OutlineThickness { get; set; } = 1f;
/// <summary>
/// Outline is like a border, but on the outside of the component
/// </summary>
[CUISerializable]
public Color OutlineColor
{
get => CUIProps.OutlineColor.Value;
set => CUIProps.OutlineColor.SetValue(value);
}
/// <summary>
/// Will be drawn in background with BackgroundColor
/// Default is solid white 1x1 texture
/// </summary>
[CUISerializable]
public CUISprite BackgroundSprite
{
get => CUIProps.BackgroundSprite.Value;
set => CUIProps.BackgroundSprite.SetValue(value);
}
/// <summary>
/// If true, mouse events on transparent pixels will be ignored
/// Note: this will buffer texture data and potentially consume a lot of memory
/// so use wisely
/// </summary>
[CUISerializable]
public bool IgnoreTransparent
{
get => CUIProps.IgnoreTransparent.Value;
set => CUIProps.IgnoreTransparent.SetValue(value);
}
//TODO i think those colors could be stored inside sprites
// But then it'll be much harder to apply side effects, think about it
/// <summary>
/// Color of BackgroundSprite, default is black
/// If you're using custom sprite and don't see it make sure this color is not black
/// </summary>
[CUISerializable]
public Color BackgroundColor
{
get => CUIProps.BackgroundColor.Value;
set => CUIProps.BackgroundColor.SetValue(value);
}
private float transparency = 1.0f;
public float Transparency
{
get => transparency;
set
{
transparency = value;
foreach (CUIComponent child in Children)
{
if (!child.IgnoreParentTransparency) child.Transparency = value;
}
}
}
/// <summary>
/// This palette will be used to resolve palette styles
/// Primary, Secondary, Tertiary, Quaternary
/// </summary>
[CUISerializable]
public PaletteOrder Palette
{
get => CUIProps.Palette.Value;
set => CUIProps.Palette.SetValue(value);
}
public PaletteOrder DeepPalette
{
set
{
Palette = value;
foreach (var child in Children)
{
child.DeepPalette = value;
}
}
}
/// <summary>
/// Had to expose resize handle props, because it's not a real component
/// and can't really use styles
/// </summary>
[CUISerializable]
public Color ResizeHandleColor { get; set; } = Color.White;
[CUISerializable]
public Color ResizeHandleGrabbedColor { get; set; } = Color.Cyan;
/// <summary>
/// don't
/// </summary>
public SamplerState SamplerState { get; set; }
}
}

View File

@@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
/// <summary>
/// Should children be cut off by scissor rect, this is just visual, it's not the same as culling
/// </summary>
[CUISerializable] public bool HideChildrenOutsideFrame { get; set; }
/// <summary>
/// if child rect doesn't intersect with parent it won't be drawn and won't consume fps
/// It also sets HideChildrenOutsideFrame
/// </summary>
[CUISerializable]
public bool CullChildren
{
get => CUIProps.CullChildren.Value;
set => CUIProps.CullChildren.SetValue(value);
}
/// <summary>
/// It shouldn't be culled off even outside of parent bounds and even if parent demands so
/// </summary>
[CUISerializable] public bool UnCullable { get; set; }
/// <summary>
/// Will shift all children by this much, e.g. this is how scroll works
/// It's also 3D
/// </summary>
[CUISerializable]
public CUI3DOffset ChildrenOffset
{
get => CUIProps.ChildrenOffset.Value;
set => CUIProps.ChildrenOffset.SetValue(value);
}
/// <summary>
/// Limits to children positions
/// </summary>
public Func<CUIRect, CUIBoundaries> ChildrenBoundaries { get; set; }
/// <summary>
/// Should it ignore child offset?
/// </summary>
[CUISerializable] public bool Fixed { get; set; }
/// <summary>
/// this point of this component
/// </summary>
[CUISerializable] public Vector2 Anchor { get; set; }
/// <summary>
/// will be attached to this point of parent
/// </summary>
[CUISerializable] public Vector2? ParentAnchor { get; set; }
/// <summary>
/// Ghost components don't affect layout
/// </summary>
[CUISerializable]
public CUIBool2 Ghost
{
get => CUIProps.Ghost.Value;
set => CUIProps.Ghost.SetValue(value);
}
/// <summary>
/// Components are drawn in order of their ZIndex
/// Normally it's derived from component position in the tree,
/// but this will override it
/// </summary>
[CUISerializable]
public int? ZIndex
{
get => CUIProps.ZIndex.Value;
set => CUIProps.ZIndex.SetValue(value);
}
/// <summary>
/// If true component will set it's Absolute size to sprite texture size
/// </summary>
[CUISerializable]
public bool ResizeToSprite
{
get => CUIProps.ResizeToSprite.Value;
set => CUIProps.ResizeToSprite.SetValue(value);
}
/// <summary>
/// Will be resized to fill empty space in list components
/// </summary>
[CUISerializable]
public CUIBool2 FillEmptySpace
{
get => CUIProps.FillEmptySpace.Value;
set => CUIProps.FillEmptySpace.SetValue(value);
}
/// <summary>
/// Will resize itself to fit components with absolute size, e.g. text
/// </summary>
[CUISerializable]
public CUIBool2 FitContent
{
get => CUIProps.FitContent.Value;
set => CUIProps.FitContent.SetValue(value);
}
/// <summary>
/// Absolute size and position in pixels
/// </summary>
[CUISerializable]
public CUINullRect Absolute
{
get => CUIProps.Absolute.Value;
set => CUIProps.Absolute.SetValue(value);
}
[CUISerializable]
public CUINullRect AbsoluteMin
{
get => CUIProps.AbsoluteMin.Value;
set => CUIProps.AbsoluteMin.SetValue(value);
}
[CUISerializable]
public CUINullRect AbsoluteMax
{
get => CUIProps.AbsoluteMax.Value;
set => CUIProps.AbsoluteMax.SetValue(value);
}
/// <summary>
/// Relative to parent size and position, [0..1]
/// </summary>
[CUISerializable]
public CUINullRect Relative
{
get => CUIProps.Relative.Value;
set => CUIProps.Relative.SetValue(value);
}
[CUISerializable]
public CUINullRect RelativeMin
{
get => CUIProps.RelativeMin.Value;
set => CUIProps.RelativeMin.SetValue(value);
}
[CUISerializable]
public CUINullRect RelativeMax
{
get => CUIProps.RelativeMax.Value;
set => CUIProps.RelativeMax.SetValue(value);
}
/// <summary>
/// It's like Relative, but to the opposite dimension
/// E.g. Real.Width = CrossRelative.Width * Parent.Real.Height
/// Handy for creating square things
/// </summary>
[CUISerializable]
public CUINullRect CrossRelative
{
get => CUIProps.CrossRelative.Value;
set => CUIProps.CrossRelative.SetValue(value);
}
/// <summary>
/// Used in Grid, space separated Row sizes, either in pixels (123) or in % (123%)
/// </summary>
[CUISerializable] public string GridTemplateRows { get; set; }
/// <summary>
/// Used in Grid, space separated Columns sizes, either in pixels (123) or in % (123%)
/// </summary>
[CUISerializable] public string GridTemplateColumns { get; set; }
/// <summary>
/// Component will be placed in this cell in the grid component
/// </summary>
[CUISerializable] public Point? GridStartCell { get; set; }
/// <summary>
/// And resized to fit cells from GridStartCell to GridEndCell
/// </summary>
[CUISerializable] public Point? GridEndCell { get; set; }
/// <summary>
/// Sets both GridStartCell and GridEndCell at once
/// </summary>
public Point? GridCell
{
get => GridStartCell;
set
{
GridStartCell = value;
GridEndCell = value;
}
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
#region Layout --------------------------------------------------------
protected CUILayout layout;
//[CUISerializable]
public virtual CUILayout Layout
{
get => layout;
set { layout = value; layout.Host = this; }
}
public event Action OnLayoutUpdated;
public void InvokeOnLayoutUpdated() => OnLayoutUpdated?.Invoke();
/// <summary>
/// Triggers recalculation of layouts from parent and below
/// </summary>
internal void OnPropChanged([CallerMemberName] string memberName = "")
{
Layout.Changed = true;
CUIDebug.Capture(null, this, "OnPropChanged", memberName, "Layout.Changed", "true");
MainComponent?.LayoutChanged();
}
internal void OnSelfAndParentChanged([CallerMemberName] string memberName = "")
{
Layout.SelfAndParentChanged = true;
CUIDebug.Capture(null, this, "OnSelfAndParentChanged", memberName, "Layout.SelfAndParentChanged", "true");
MainComponent?.LayoutChanged();
}
/// <summary>
/// Triggers recalc of own pseudo components and nothing else
/// </summary>
internal void OnDecorPropChanged([CallerMemberName] string memberName = "")
{
Layout.DecorChanged = true;
CUIDebug.Capture(null, this, "OnDecorPropChanged", memberName, "Layout.DecorChanged", "true");
MainComponent?.LayoutChanged();
}
/// <summary>
/// Notifies parent (only) than it may need to ResizeToContent
/// </summary>
internal void OnAbsolutePropChanged([CallerMemberName] string memberName = "")
{
Layout.AbsoluteChanged = true;
CUIDebug.Capture(null, this, "OnAbsolutePropChanged", memberName, "Layout.AbsoluteChanged", "true");
MainComponent?.LayoutChanged();
}
/// <summary>
/// Triggers recalculation of layouts from this and below
/// </summary>
internal void OnChildrenPropChanged([CallerMemberName] string memberName = "")
{
Layout.ChildChanged = true;
MainComponent?.LayoutChanged();
}
#endregion
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
//HACK This is potentially cursed
/// <summary>
/// Arbitrary data
/// </summary>
public object Data { get; set; }
/// <summary>
/// Will prevent serialization to xml if true
/// </summary>
public bool Unserializable { get; set; }
/// <summary>
/// Is this a serialization cutoff point
/// Parent will serialize children down to this component
/// Further serialization should be hadled by this component
/// </summary>
[CUISerializable] public bool BreakSerialization { get; set; }
/// <summary>
/// Some props (like visible) are autopassed to all new childs
/// see PassPropsToChild
/// </summary>
[CUISerializable] public bool ShouldPassPropsToChildren { get; set; } = true;
/// <summary>
/// Don't inherit parent Visibility
/// </summary>
[CUISerializable] public bool IgnoreParentVisibility { get; set; }
/// <summary>
/// Don't inherit parent IgnoreEvents
/// </summary>
[CUISerializable] public bool IgnoreParentEventIgnorance { get; set; }
/// <summary>
/// Don't inherit parent ZIndex
/// </summary>
[CUISerializable] public bool IgnoreParentZIndex { get; set; }
[CUISerializable] public bool IgnoreParentTransparency { get; set; }
/// <summary>
/// Invisible components are not drawn, but still can be interacted with
/// </summary>
[CUISerializable]
public bool Visible
{
get => CUIProps.Visible.Value;
set => CUIProps.Visible.SetValue(value);
}
/// <summary>
/// Won't react to mouse events
/// </summary>
[CUISerializable]
public bool IgnoreEvents
{
get => CUIProps.IgnoreEvents.Value;
set => CUIProps.IgnoreEvents.SetValue(value);
}
/// <summary>
/// Visible + !IgnoreEvents
/// </summary>
public bool Revealed
{
get => CUIProps.Revealed.Value;
set => CUIProps.Revealed.SetValue(value);
}
//HACK this is meant for buttons, but i want to access it on generic components in CUIMap
protected bool disabled;
/// <summary>
/// Usually means - non interactable, e.g. unclickable gray button
/// </summary>
[CUISerializable]
public virtual bool Disabled
{
get => disabled;
set => disabled = value;
}
}
}

View File

@@ -0,0 +1,468 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent
{
public record CompareResult(bool equal, string firstMismatch = "")
{
public static implicit operator bool(CompareResult r) => r.equal;
}
public static bool DeepCompareVerbose(CUIComponent a, CUIComponent b)
{
CompareResult result = DeepCompare(a, b);
if (result.equal) CUI.Log($"{a} == {b}");
else CUI.Log($"{result.firstMismatch}");
return result.equal;
}
public static CompareResult DeepCompare(CUIComponent a, CUIComponent b)
{
if (a.GetType() != b.GetType()) return new CompareResult(false, $"type mismatch: {a} | {b}");
Type T = a.GetType();
CUITypeMetaData meta = CUITypeMetaData.Get(T);
foreach (var (key, pi) in meta.Serializable)
{
if (!object.Equals(pi.GetValue(a), pi.GetValue(b)))
{
return new CompareResult(false, $"{pi}: {a}{pi.GetValue(a)} | {b}{pi.GetValue(b)}");
}
}
if (a.Children.Count != b.Children.Count)
{
return new CompareResult(false, $"child count mismatch: {a}{CUI.ArrayToString(a.Children)} | {b}{CUI.ArrayToString(b.Children)}");
}
for (int i = 0; i < a.Children.Count; i++)
{
CompareResult sub = DeepCompare(a.Children[i], b.Children[i]);
if (!sub.equal) return sub;
}
return new CompareResult(true);
}
#region State --------------------------------------------------------
/// <summary>
/// State is just a clone component with copies of all props
/// </summary>
public Dictionary<string, CUIComponent> States { get; set; } = new();
// TODO why all clones are unreal? this is sneaky, and i don't remember what's it for
public CUIComponent Clone()
{
CUIComponent clone = new CUIComponent()
{
Unreal = true,
};
clone.ApplyState(this);
return clone;
}
public void SaveStateAs(string name) => States[name] = this.Clone();
public void LoadState(string name) => ApplyState(States.GetValueOrDefault(name));
public void ForgetState(string name) => States.Remove(name);
//TODO think about edge cases (PassPropsToChild)
public void ApplyState(CUIComponent state)
{
Stopwatch sw = Stopwatch.StartNew();
if (state == null) return;
//TODO why not closest relative?
Type targetType = state.GetType() == GetType() ? GetType() : typeof(CUIComponent);
CUITypeMetaData meta = CUITypeMetaData.Get(targetType);
//TODO Megacringe, fix it
foreach (PropertyInfo pi in meta.Serializable.Values)
{
if (pi.PropertyType.IsValueType || pi.PropertyType == typeof(string))
{
pi.SetValue(this, pi.GetValue(state));
}
else
{
object value = pi.GetValue(state);
if (value == null)
{
pi.SetValue(this, null);
continue;
}
if (pi.PropertyType.IsAssignableTo(typeof(ICloneable)))
{
ICloneable cloneable = (ICloneable)pi.GetValue(state);
object clone = cloneable.Clone();
pi.SetValue(this, clone);
}
else
{
CUI.Info($"Ekhem, can't copy {pi} prop from {state} to {this} because it's not cloneable");
}
}
}
//TODO Megacringe, fix it
foreach (PropertyInfo pi in meta.Serializable.Values)
{
if (pi.PropertyType.IsValueType && !object.Equals(pi.GetValue(state), pi.GetValue(this)))
{
pi.SetValue(this, pi.GetValue(state));
}
}
}
#endregion
#region XML --------------------------------------------------------
public static bool ForceSaveAllProps { get; set; } = false;
public static bool SaveAfterLoad { get; set; } = true;
public string SavePath { get; set; }
public virtual XElement ToXML(CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
try
{
if (Unserializable) return null;
Type type = GetType();
XElement e = new XElement(type.Name);
PackProps(e, propAttribute);
foreach (CUIComponent child in Children)
{
if (!this.BreakSerialization)
{
e.Add(child.ToXML(propAttribute));
}
}
return e;
}
catch (Exception e)
{
CUI.Warning(e);
return new XElement("Error", e.Message);
}
}
public virtual void FromXML(XElement element, string baseFolder = null)
{
foreach (XElement childElement in element.Elements())
{
Type childType = CUIReflection.GetComponentTypeByName(childElement.Name.ToString());
if (childType == null) continue;
CUIComponent child = (CUIComponent)Activator.CreateInstance(childType);
child.FromXML(childElement, baseFolder);
//CUI.Log($"{this}[{child.AKA}] = {child} ");
this.Append(child, child.AKA);
}
ExtractProps(element, baseFolder);
}
protected void ExtractProps(XElement element, string baseFolder = null)
{
Type type = GetType();
CUITypeMetaData meta = CUITypeMetaData.Get(type);
foreach (XAttribute attribute in element.Attributes())
{
if (!meta.Serializable.ContainsKey(attribute.Name.ToString()))
{
CUIDebug.Error($"Can't parse prop {attribute.Name} in {type.Name} because type metadata doesn't contain that prop (is it a property? fields aren't supported yet)");
continue;
}
PropertyInfo prop = meta.Serializable[attribute.Name.ToString()];
MethodInfo parse = null;
if (CUIExtensions.Parse.ContainsKey(prop.PropertyType))
{
parse = CUIExtensions.Parse[prop.PropertyType];
}
parse ??= prop.PropertyType.GetMethod(
"Parse",
BindingFlags.Public | BindingFlags.Static,
new Type[] { typeof(string) }
);
Func<string, object> ParseWithContext = null;
//HACK
if (prop.PropertyType == typeof(CUISprite) && baseFolder != null)
{
ParseWithContext = (raw) => CUISprite.ParseWithContext(raw, baseFolder);
}
if (parse == null)
{
if (prop.PropertyType.IsEnum)
{
try
{
prop.SetValue(this, Enum.Parse(prop.PropertyType, attribute.Value));
}
catch (Exception e)
{
CUIDebug.Error($"Can't parse {attribute.Value} into {prop.PropertyType.Name}\n{e}");
}
}
else
{
CUIDebug.Error($"Can't parse prop {prop.Name} in {type.Name} because it's type {prop.PropertyType.Name} is missing Parse method");
}
}
else
{
try
{
object result = null;
if (ParseWithContext != null)
{
result = ParseWithContext(attribute.Value);
}
else
{
result = parse.Invoke(null, new object[] { attribute.Value });
}
prop.SetValue(this, result);
}
catch (Exception e)
{
CUIDebug.Error($"Can't parse {attribute.Value} into {prop.PropertyType.Name}\n{e}");
}
}
}
}
protected void PackProps(XElement element, CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
Type type = GetType();
CUITypeMetaData meta = CUITypeMetaData.Get(type);
SortedDictionary<string, PropertyInfo> props = propAttribute switch
{
CUIAttribute.CUISerializable => meta.Serializable,
CUIAttribute.Calculated => meta.Calculated,
_ => meta.Serializable,
};
foreach (string key in props.Keys)
{
try
{
object value = props[key].GetValue(this);
// it's default value for this prop
if (!ForceSaveAllProps && meta.Default != null && Object.Equals(value, CUIReflection.GetNestedValue(meta.Default, key)))
{
continue;
}
MethodInfo customToString = CUIExtensions.CustomToString.GetValueOrDefault(props[key].PropertyType);
if (customToString != null)
{
element?.SetAttributeValue(key, customToString.Invoke(null, new object[] { value }));
}
else
{
element?.SetAttributeValue(key, value);
}
}
catch (Exception e)
{
CUI.Warning($"Failed to serialize prop: {e.Message}");
CUI.Warning($"{key} in {this}");
}
}
}
public string Serialize(CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
try
{
XElement e = this.ToXML(propAttribute);
return e.ToString();
}
catch (Exception e)
{
CUI.Error(e);
return e.Message;
}
}
public static CUIComponent Deserialize(string raw, string baseFolder = null)
{
return Deserialize(XElement.Parse(raw));
}
public static CUIComponent Deserialize(XElement e, string baseFolder = null)
{
try
{
Type type = CUIReflection.GetComponentTypeByName(e.Name.ToString());
if (type == null) return null;
CUIComponent c = (CUIComponent)Activator.CreateInstance(type);
// c.RemoveAllChildren();
c.FromXML(e, baseFolder);
CUIComponent.RunRecursiveOn(c, (component) => component.Hydrate());
return c;
}
catch (Exception ex)
{
CUIDebug.Error(ex);
return null;
}
}
public void LoadSelfFromFile(string path, bool searchForSpritesInTheSameFolder = true, bool saveAfterLoad = false)
{
try
{
XDocument xdoc = XDocument.Load(path);
RemoveAllChildren();
if (searchForSpritesInTheSameFolder) FromXML(xdoc.Root, Path.GetDirectoryName(path));
else FromXML(xdoc.Root);
CUIComponent.RunRecursiveOn(this, (component) => component.Hydrate());
SavePath = path;
if (SaveAfterLoad && saveAfterLoad) SaveToTheSamePath();
}
catch (Exception ex)
{
CUI.Warning(ex);
}
}
public static CUIComponent LoadFromFile(string path, bool searchForSpritesInTheSameFolder = true, bool saveAfterLoad = false)
{
try
{
XDocument xdoc = XDocument.Load(path);
CUIComponent result;
if (searchForSpritesInTheSameFolder)
{
result = Deserialize(xdoc.Root, Path.GetDirectoryName(path));
}
else result = Deserialize(xdoc.Root);
result.SavePath = path;
if (SaveAfterLoad && saveAfterLoad) result.SaveToTheSamePath();
return result;
}
catch (Exception ex)
{
CUIDebug.Error(ex);
return null;
}
}
public static T LoadFromFile<T>(string path, bool searchForSpritesInTheSameFolder = true, bool saveAfterLoad = false) where T : CUIComponent
{
try
{
XDocument xdoc = XDocument.Load(path);
T result;
if (searchForSpritesInTheSameFolder)
{
result = (T)Deserialize(xdoc.Root, Path.GetDirectoryName(path));
}
else result = (T)Deserialize(xdoc.Root);
result.SavePath = path;
if (SaveAfterLoad && saveAfterLoad) result.SaveToTheSamePath();
return result;
}
catch (Exception ex)
{
CUIDebug.Error(ex);
return null;
}
}
public void LoadFromTheSameFile()
{
if (SavePath == null)
{
CUI.Warning($"Can't load {this} from The Same Path, SavePath is null");
return;
}
LoadSelfFromFile(SavePath);
}
public void SaveToTheSamePath()
{
if (SavePath == null)
{
CUI.Warning($"Can't save {this} To The Same Path, SavePath is null");
return;
}
SaveToFile(SavePath);
}
public void SaveToFile(string path, CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
try
{
XDocument xdoc = new XDocument();
xdoc.Add(this.ToXML(propAttribute));
xdoc.Save(path);
SavePath = path;
}
catch (Exception e)
{
CUI.Warning(e);
}
}
/// <summary>
/// Experimental method
/// Here you can add data/ callbacks/ save stuff to variables
/// after loading a xml skeletom
/// </summary>
public virtual void Hydrate()
{
}
#endregion
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent : IDisposable
{
private void SetupStyles()
{
Style = new CUIStyle();
}
/// <summary>
/// Use it to e.g. update component color
/// </summary>
public event Action OnStyleApplied;
internal void InvokeOnStyleApplied() => OnStyleApplied?.Invoke();
private void HandleStylePropChange(string key, string value)
{
CUIGlobalStyleResolver.OnComponentStylePropChanged(this, key);
}
private void HandleStyleChange(CUIStyle s)
{
CUIGlobalStyleResolver.OnComponentStyleChanged(this);
}
private CUIStyle style;
/// <summary>
/// Allows you to assing parsable string or link to CUIPalette to any prop
/// It's indexable, so you can access it like this: component.Style["BackgroundColor"] = "cyan"
/// if value starts with "CUIPalette." it will extract the value from palette
/// e.g. component.Style["BackgroundColor"] = "CUIPalette.DarkBlue.Secondary.On"
/// </summary>
[CUISerializable]
public CUIStyle Style
{
get => style;
set
{
if (style == value) return;
if (style != null)
{
style.OnUse -= HandleStyleChange;
style.OnPropChanged -= HandleStylePropChange;
}
style = value;
if (style != null)
{
style.OnUse += HandleStyleChange;
style.OnPropChanged += HandleStylePropChange;
}
HandleStyleChange(style);
}
}
public CUIStyle ResolvedStyle { get; set; }
}
}

View File

@@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
public partial class CUIComponent
{
#region Tree --------------------------------------------------------
public List<CUIComponent> Children { get; set; } = new();
private CUIComponent? parent; public CUIComponent? Parent
{
get => parent;
set => SetParent(value);
}
internal void SetParent(CUIComponent? value, [CallerMemberName] string memberName = "")
{
if (parent != null)
{
TreeChanged = true;
OnPropChanged();
parent.Forget(this);
parent.Children.Remove(this);
parent.OnChildRemoved?.Invoke(this);
}
parent = value;
CUIDebug.Capture(null, this, "SetParent", memberName, "parent", $"{parent}");
if (parent != null)
{
if (parent is CUIMainComponent main) MainComponent = main;
if (parent?.MainComponent != null) MainComponent = parent.MainComponent;
//parent.Children.Add(this);
TreeChanged = true;
if (AKA != null) parent.Remember(this, AKA);
parent.PassPropsToChild(this);
OnPropChanged();
parent.OnChildAdded?.Invoke(this);
}
}
private bool treeChanged = true; internal bool TreeChanged
{
get => treeChanged;
set
{
treeChanged = value;
if (value)
{
OnTreeChanged?.Invoke();
if (Parent != null) Parent.TreeChanged = true;
}
}
}
/// <summary>
/// Allows you to add array of children
/// </summary>
public IEnumerable<CUIComponent> AddChildren
{
set
{
foreach (CUIComponent c in value) { Append(c); }
}
}
public event Action<CUIComponent> OnChildAdded;
public event Action<CUIComponent> OnChildRemoved;
/// <summary>
/// Adds children to the end of the list
/// </summary>
/// <param name="child"></param>
/// <param name="name"> AKA </param>
/// <returns> child </returns>
public virtual CUIComponent Append(CUIComponent child, string name = null, [CallerMemberName] string memberName = "")
{
if (child == null) return child;
child.Parent = this;
Children.Add(child);
if (name != null) Remember(child, name);
return child;
}
/// <summary>
/// Adds children to the begining of the list
/// </summary>
/// <param name="child"></param>
/// <param name="name"> AKA </param>
/// <returns> child </returns>
public virtual CUIComponent Prepend(CUIComponent child, string name = null, [CallerMemberName] string memberName = "")
{
if (child == null) return child;
child.Parent = this;
Children.Insert(0, child);
if (name != null) Remember(child, name);
return child;
}
public virtual CUIComponent Insert(CUIComponent child, int index, string name = null, [CallerMemberName] string memberName = "")
{
if (child == null) return child;
child.Parent = this;
index = Math.Clamp(index, 0, Children.Count);
Children.Insert(index, child);
if (name != null) Remember(child, name);
return child;
}
//TODO DRY
public void RemoveSelf() => Parent?.RemoveChild(this);
public CUIComponent RemoveChild(CUIComponent child, [CallerMemberName] string memberName = "")
{
if (child == null || !Children.Contains(child)) return child;
if (this != null) // kek
{
child.TreeChanged = true;
child.OnPropChanged();
//HACK i'm sure it doesn't belong here, find a better place
forsedSize = new CUINullVector2();
OnAbsolutePropChanged();
// Forget(child);
Children.Remove(child);
OnChildRemoved?.Invoke(child);
}
child.parent = null;
CUIDebug.Capture(null, this, "RemoveChild", memberName, "child", $"{child}");
return child;
}
//TODO DRY
public void RemoveAllChildren([CallerMemberName] string memberName = "")
{
foreach (CUIComponent c in Children)
{
if (this != null) // kek
{
c.TreeChanged = true;
c.OnPropChanged();
//Forget(c);
//Children.Remove(c);
OnChildRemoved?.Invoke(c);
}
c.parent = null;
CUIDebug.Capture(null, this, "RemoveAllChildren", memberName, "child", $"{c}");
}
NamedComponents.Clear();
Children.Clear();
}
/// <summary>
/// Pass props like ZIndex, Visible to a new child
/// </summary>
/// <param name="child"></param>
protected virtual void PassPropsToChild(CUIComponent child)
{
if (!ShouldPassPropsToChildren) return;
//child.Palette = Palette;
if (ZIndex.HasValue && !child.IgnoreParentZIndex) child.ZIndex = ZIndex.Value + 1;
if (IgnoreEvents && !child.IgnoreParentEventIgnorance) child.IgnoreEvents = true;
if (!Visible && !child.IgnoreParentVisibility) child.Visible = false;
}
#endregion
}
}

View File

@@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
using System.Threading;
namespace QICrabUI
{
/// <summary>
/// Base class for all components
/// </summary>
public partial class CUIComponent : IDisposable
{
#region Static --------------------------------------------------------
internal static void InitStatic()
{
CUI.OnInit += () =>
{
MaxID = 0;
};
CUI.OnDispose += () =>
{
foreach (int id in ComponentsById.Keys)
{
CUIComponent component = null;
ComponentsById[id].TryGetTarget(out component);
component?.Dispose();
}
ComponentsById.Clear();
ComponentsByType.Clear();
dummyComponent = null;
};
}
internal static int MaxID;
public static Dictionary<int, WeakReference<CUIComponent>> ComponentsById = new();
public static WeakCatalog<Type, CUIComponent> ComponentsByType = new();
/// <summary>
/// This is used to trick vanilla GUI into believing that
/// mouse is hovering some component and block clicks
/// </summary>
public static GUIButton dummyComponent = new GUIButton(new RectTransform(new Point(0, 0)))
{
Text = "DUMMY",
};
/// <summary>
/// designed to be versatile, in fact never used
/// </summary>
public static void RunRecursiveOn(CUIComponent component, Action<CUIComponent> action)
{
action(component);
foreach (CUIComponent child in component.Children)
{
RunRecursiveOn(child, action);
}
}
public static void ForEach(Action<CUIComponent> action)
{
foreach (int id in ComponentsById.Keys)
{
CUIComponent component = null;
ComponentsById[id].TryGetTarget(out component);
if (component is not null) action(component);
}
}
public static IEnumerable<Type> GetClassHierarchy(Type type)
{
while (type != typeof(Object) && type != null)
{
yield return type;
type = type.BaseType;
}
}
public static IEnumerable<Type> GetReverseClassHierarchy(Type type)
=> CUIComponent.GetClassHierarchy(type).Reverse<Type>();
#endregion
#region Virtual --------------------------------------------------------
//TODO move to cui props, it's a bit more clampicated than ChildrenBoundaries
/// <summary>
/// Bounds for offset, e.g. scroll, zoom
/// </summary>
internal virtual CUIBoundaries ChildOffsetBounds => new CUIBoundaries();
/// <summary>
/// "Component like" ghost stuff that can't have children and
/// doesn't impact layout. Drag handles, text etc
/// </summary>
internal virtual void UpdatePseudoChildren()
{
LeftResizeHandle.Update();
RightResizeHandle.Update();
}
/// <summary>
/// Last chance to disagree with proposed size
/// For stuff that should resize to content
/// </summary>
/// <param name="size"> proposed size </param>
/// <returns> size you're ok with </returns>
internal virtual Vector2 AmIOkWithThisSize(Vector2 size) => size;
/// <summary>
/// Here component should be drawn
/// </summary>
/// <param name="spriteBatch"></param>
public virtual partial void Draw(SpriteBatch spriteBatch);
/// <summary>
/// Method for drawing something that should always be on top, e.g. resize handles
/// </summary>
/// <param name="spriteBatch"></param>
public virtual partial void DrawFront(SpriteBatch spriteBatch);
#endregion
#region Draw --------------------------------------------------------
public virtual partial void Draw(SpriteBatch spriteBatch)
{
if (BackgroundVisible) CUI.DrawRectangle(spriteBatch, Real, BackgroundColor * Transparency, BackgroundSprite);
CUI.DrawBorders(spriteBatch, this);
// if (Border.Visible) GUI.DrawRectangle(spriteBatch, BorderBox.Position, BorderBox.Size, Border.Color, thickness: Border.Thickness);
if (OutlineVisible) GUI.DrawRectangle(spriteBatch, OutlineBox.Position, OutlineBox.Size, OutlineColor, thickness: OutlineThickness);
LeftResizeHandle.Draw(spriteBatch);
RightResizeHandle.Draw(spriteBatch);
}
public virtual partial void DrawFront(SpriteBatch spriteBatch)
{
if (DebugHighlight)
{
GUI.DrawRectangle(spriteBatch, Real.Position, Real.Size, Color.Cyan * 0.5f, isFilled: true);
}
}
#endregion
#region Constructors --------------------------------------------------------
internal void Vitalize()
{
foreach (FieldInfo fi in this.GetType().GetFields(AccessTools.all))
{
if (fi.FieldType.IsAssignableTo(typeof(ICUIVitalizable)))
{
ICUIVitalizable prop = (ICUIVitalizable)fi.GetValue(this);
if (prop == null) continue;
prop.SetHost(this);
}
}
}
internal void VitalizeProps()
{
foreach (FieldInfo fi in this.GetType().GetFields(AccessTools.all))
{
if (fi.FieldType.IsAssignableTo(typeof(ICUIProp)))
{
ICUIProp prop = (ICUIProp)fi.GetValue(this);
if (prop == null) continue; // this is for Main.GrabbedDragHandle
prop.SetHost(this);
prop.SetName(fi.Name);
}
}
foreach (FieldInfo fi in typeof(CUIComponentProps).GetFields(AccessTools.all))
{
if (fi.FieldType.IsAssignableTo(typeof(ICUIProp)))
{
ICUIProp prop = (ICUIProp)fi.GetValue(CUIProps);
if (prop == null) continue;
prop.SetHost(this);
prop.SetName(fi.Name);
}
}
}
public CUIComponent()
{
if (CUI.Disposed)
{
Disposed = true;
return;
}
ID = MaxID++;
ComponentsById[ID] = new WeakReference<CUIComponent>(this);
ComponentsByType.Add(this.GetType(), this);
Vitalize();
VitalizeProps();
SetupCommands();
Layout = new CUILayoutSimple();
SetupStyles();
SetupAnimations();
}
public CUIComponent(float? x = null, float? y = null, float? w = null, float? h = null) : this()
{
Relative = new CUINullRect(x, y, w, h);
}
public bool Disposed;
public void Dispose()
{
if (Disposed) return;
CleanUp();
Disposed = true;
}
public virtual void CleanUp() { }
~CUIComponent() => Dispose();
public override string ToString() => $"{this.GetType().Name}:{ID}:{AKA}";
#endregion
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
/// <summary>
/// Drop down list, aka Select
/// </summary>
public class CUIDropDown : CUIComponent
{
internal class DDOption : CUIButton
{
public DDOption() : this("") { }
public DDOption(string text) : base(text) { }
}
private CUIButton MainButton;
private CUIVerticalList OptionBox;
/// <summary>
/// List of options
/// Options are just strings
/// </summary>
[CUISerializable]
public IEnumerable<string> Options
{
get => OptionBox.Children.Cast<DDOption>().Select(o => o.Text);
set
{
Clear();
foreach (string option in value) { Add(option); }
}
}
[CUISerializable]
public string Selected
{
get => MainButton.Text;
set => Select(value);
}
public event Action<string> OnSelect;
public Action<string> AddOnSelect { set { OnSelect += value; } }
public void Open() => OptionBox.Revealed = true;
public void Close() => OptionBox.Revealed = false;
public void Clear()
{
OptionBox.RemoveAllChildren();
Select("");
}
public void Add(string option)
{
OptionBox.Append(new DDOption(option)
{
AddOnMouseDown = (e) => Select(option),
});
}
public void Select(int i) => Select(Options.ElementAtOrDefault(i));
public void Select(string option)
{
MainButton.Text = option ?? "";
OptionBox.Revealed = false;
OnSelect?.Invoke(MainButton.Text);
}
public void Remove(int i) => Remove(Options.ElementAtOrDefault(i));
public void Remove(string option)
{
if (option == null) return;
if (!Options.Contains(option)) return;
DDOption ddoption = OptionBox.Children.Cast<DDOption>().FirstOrDefault(o => o.Text == option);
bool wasSelected = MainButton.Text == ddoption.Text;
OptionBox.RemoveChild(ddoption);
if (wasSelected) Select(0);
}
public CUIDropDown() : base()
{
BreakSerialization = true;
OptionBox = new CUIVerticalList()
{
Relative = new CUINullRect(w: 1),
FitContent = new CUIBool2(true, true),
Ghost = new CUIBool2(false, true),
Anchor = CUIAnchor.TopLeft,
ParentAnchor = CUIAnchor.BottomLeft,
ZIndex = 500,
Style = new CUIStyle(){
{"BackgroundColor", "CUIPalette.DDOption.Background"},
{"Border", "CUIPalette.DDOption.Border"},
},
};
MainButton = new CUIButton()
{
Text = "CUIDropDown",
Relative = new CUINullRect(w: 1, h: 1),
AddOnMouseDown = (e) => OptionBox.Revealed = !OptionBox.Revealed,
};
Append(MainButton);
Append(OptionBox);
FitContent = new CUIBool2(true, true);
//HACK Why this main is hardcoded?
//in static constructor CUI.Main is null and this won't work
if (CUI.Main is not null) CUI.Main.OnMouseDown += (e) => Close();
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Draggable and resizable container for other components
/// </summary>
public class CUIFrame : CUIComponent
{
public override void Draw(SpriteBatch spriteBatch)
{
if (BackgroundVisible) CUI.DrawRectangle(spriteBatch, Real, BackgroundColor, BackgroundSprite);
}
public override void DrawFront(SpriteBatch spriteBatch)
{
//if (BorderVisible) CUI.DrawBorders(spriteBatch, Real, BorderColor, BorderSprite, BorderThickness);
// GUI.DrawRectangle(spriteBatch, BorderBox.Position, BorderBox.Size, BorderColor, thickness: BorderThickness);
CUI.DrawBorders(spriteBatch, this);
if (OutlineVisible) GUI.DrawRectangle(spriteBatch, OutlineBox.Position, OutlineBox.Size, OutlineColor, thickness: OutlineThickness);
LeftResizeHandle.Draw(spriteBatch);
RightResizeHandle.Draw(spriteBatch);
//base.DrawFront(spriteBatch);
}
public event Action OnOpen;
public event Action OnClose;
/// <summary>
/// This will reveal the frame and append it to CUI.Main
/// </summary>
public void Open()
{
if (CUI.Main == null && Parent != CUI.Main) return;
CUI.Main.Append(this);
Revealed = true;
OnOpen?.Invoke();
}
/// <summary>
/// This will hide the frame and remove it from children of CUI.Main
/// </summary>
public void Close()
{
RemoveSelf();
Revealed = false;
OnClose?.Invoke();
}
public CUIFrame() : base()
{
CullChildren = true;
Resizible = true;
Draggable = true;
}
public CUIFrame(float? x = null, float? y = null, float? w = null, float? h = null) : this()
{
Relative = new CUINullRect(x, y, w, h);
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// A Grid containing children in its cells
/// </summary>
public class CUIGrid : CUIComponent
{
public override CUILayout Layout
{
get => layout;
set
{
layout = new CUILayoutGrid();
layout.Host = this;
}
}
public CUILayoutGrid GridLayout => (CUILayoutGrid)Layout;
public CUIGrid() : base()
{
//Layout = new CUILayoutGrid();
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Resizing components to it's Height and placing them sequentially
/// </summary>
public class CUIHorizontalList : CUIComponent
{
[CUISerializable] public bool Scrollable { get; set; }
[CUISerializable] public float ScrollSpeed { get; set; } = 1.0f;
public float LeftGap = 0f;
public float RightGap = 0f;
public override CUILayout Layout
{
get => layout;
set
{
layout = new CUILayoutHorizontalList();
layout.Host = this;
}
}
public CUILayoutHorizontalList ListLayout => (CUILayoutHorizontalList)Layout;
[CUISerializable]
public CUIDirection Direction
{
get => ListLayout.Direction;
set => ListLayout.Direction = value;
}
[CUISerializable]
public bool ResizeToHostHeight
{
get => ListLayout.ResizeToHostHeight;
set => ListLayout.ResizeToHostHeight = value;
}
public float Scroll
{
get => ChildrenOffset.X;
set
{
if (!Scrollable) return;
CUIProps.ChildrenOffset.SetValue(
ChildrenOffset with { X = value }
);
}
}
internal override CUIBoundaries ChildOffsetBounds => new CUIBoundaries(
minY: 0,
maxY: 0,
minX: LeftGap,
maxX: Math.Min(Real.Width - ListLayout.TotalWidth - RightGap, 0)
);
public CUIHorizontalList() : base()
{
CullChildren = true;
OnScroll += (m) => Scroll += m.Scroll * ScrollSpeed;
ChildrenBoundaries = CUIBoundaries.HorizontalTube;
}
}
}

View File

@@ -0,0 +1,522 @@
#define SHOWPERF
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Diagnostics;
namespace QICrabUI
{
/// <summary>
/// Orchestrating drawing and updating of it's children
/// Also a CUIComponent, but it's draw and update methods
/// Attached directly to games life cycle
/// </summary>
public class CUIMainComponent : CUIComponent
{
/// <summary>
/// Wrapper for global events
/// </summary>
public class CUIGlobalEvents
{
public Action<CUIInput> OnMouseDown; public void InvokeOnMouseDown(CUIInput e) => OnMouseDown?.Invoke(e);
public Action<CUIInput> OnMouseUp; public void InvokeOnMouseUp(CUIInput e) => OnMouseUp?.Invoke(e);
public Action<CUIInput> OnMouseMoved; public void InvokeOnMouseMoved(CUIInput e) => OnMouseMoved?.Invoke(e);
public Action<CUIInput> OnClick; public void InvokeOnClick(CUIInput e) => OnClick?.Invoke(e);
public Action<CUIInput> OnKeyDown; public void InvokeOnKeyDown(CUIInput e) => OnKeyDown?.Invoke(e);
public Action<CUIInput> OnKeyUp; public void InvokeOnKeyUp(CUIInput e) => OnKeyUp?.Invoke(e);
}
/// <summary>
/// Frozen window doesn't update
/// </summary>
public bool Frozen { get; set; }
public double UpdateInterval = 1.0 / 300.0;
/// <summary>
/// If true will update layout until it settles to prevent blinking
/// </summary>
public bool CalculateUntilResolved = true;
/// <summary>
/// If your GUI needs more than this steps of layout update
/// you will get a warning
/// </summary>
public int MaxLayoutRecalcLoopsPerUpdate = 10;
public event Action OnTreeChanged;
public Action AddOnTreeChanged { set { OnTreeChanged += value; } }
public CUIDragHandle GrabbedDragHandle;
public CUIResizeHandle GrabbedResizeHandle;
public CUISwipeHandle GrabbedSwipeHandle;
public CUIComponent MouseOn;
public CUIComponent FocusedComponent
{
get => CUI.FocusedComponent;
set => CUI.FocusedComponent = value;
}
/// <summary>
/// Container for true global events
/// CUIMainComponent itself can react to events and you can listen for those,
/// but e.g. mouse events may be consumed before they reach Main
/// </summary>
public CUIGlobalEvents Global = new CUIGlobalEvents();
private Stopwatch sw = new Stopwatch();
internal List<CUIComponent> Flat = new List<CUIComponent>();
internal List<CUIComponent> Leaves = new List<CUIComponent>();
internal SortedList<int, List<CUIComponent>> Layers = new SortedList<int, List<CUIComponent>>();
private List<CUIComponent> MouseOnList = new List<CUIComponent>();
private Vector2 GrabbedOffset;
private void RunStraigth(Action<CUIComponent> a) { for (int i = 0; i < Flat.Count; i++) a(Flat[i]); }
private void RunReverse(Action<CUIComponent> a) { for (int i = Flat.Count - 1; i >= 0; i--) a(Flat[i]); }
private void FlattenTree()
{
int retries = 0;
bool done = false;
do
{
retries++;
if (retries > 10) break;
try
{
Flat.Clear();
Layers.Clear();
int globalIndex = 0;
void CalcZIndexRec(CUIComponent component, int added = 0)
{
component.positionalZIndex = globalIndex;
globalIndex += 1;
component.addedZIndex = added;
if (component.ZIndex.HasValue) component.addedZIndex += component.ZIndex.Value;
foreach (CUIComponent child in component.Children)
{
CalcZIndexRec(child, component.addedZIndex);
}
}
CalcZIndexRec(this, 0);
RunRecursiveOn(this, (c) =>
{
int i = c.positionalZIndex + c.addedZIndex;
if (!Layers.ContainsKey(i)) Layers[i] = new List<CUIComponent>();
Layers[i].Add(c);
});
foreach (var layer in Layers)
{
Flat.AddRange(layer.Value);
}
done = true;
}
catch (Exception e)
{
CUI.Warning($"Couldn't Flatten component tree: {e.Message}");
}
} while (!done);
}
#region Update
internal bool GlobalLayoutChanged;
internal void LayoutChanged() => GlobalLayoutChanged = true;
private double LastUpdateTime;
private int UpdateLoopCount = 0;
/// <summary>
/// Forses 1 layout update step, even when Frozen
/// </summary>
public void Step()
{
Update(LastUpdateTime + UpdateInterval, true, true);
}
public void Update(double totalTime, bool force = false, bool noInput = false)
{
if (!force)
{
if (Frozen) return;
if (totalTime - LastUpdateTime <= UpdateInterval) return;
}
CUIDebug.Flush();
if (TreeChanged)
{
OnTreeChanged?.Invoke();
FlattenTree();
TreeChanged = false;
}
if (!noInput) HandleInput(totalTime);
RunStraigth(c => c.InvokeOnUpdate(totalTime));
if (CalculateUntilResolved)
{
UpdateLoopCount = 0;
do
{
GlobalLayoutChanged = false;
if (TreeChanged)
{
OnTreeChanged?.Invoke();
FlattenTree();
TreeChanged = false;
}
RunReverse(c =>
{
c.Layout.ResizeToContent();
});
RunStraigth(c =>
{
c.Layout.Update();
c.Layout.UpdateDecor();
});
UpdateLoopCount++;
if (UpdateLoopCount >= MaxLayoutRecalcLoopsPerUpdate)
{
PrintRecalLimitWarning();
break;
}
}
while (GlobalLayoutChanged);
//CUI.Log($"UpdateLoopCount: {UpdateLoopCount}");
}
else
{
RunReverse(c =>
{
c.Layout.ResizeToContent();
});
RunStraigth(c =>
{
c.Layout.Update();
c.Layout.UpdateDecor();
});
}
//TODO do i need 2 updates?
//RunStraigth(c => c.InvokeOnUpdate(totalTime));
LastUpdateTime = totalTime;
}
#endregion
#region Draw
private void StopStart(SpriteBatch spriteBatch, Rectangle SRect, SamplerState? samplerState = null)
{
samplerState ??= GUI.SamplerState;
spriteBatch.End();
spriteBatch.GraphicsDevice.ScissorRectangle = SRect;
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: samplerState, rasterizerState: GameMain.ScissorTestEnable);
}
public new void Draw(SpriteBatch spriteBatch)
{
sw.Restart();
Rectangle OriginalSRect = spriteBatch.GraphicsDevice.ScissorRectangle;
Rectangle SRect = OriginalSRect;
try
{
RunStraigth(c =>
{
if (!c.Visible || c.CulledOut) return;
if (c.Parent != null && c.Parent.ScissorRect.HasValue && SRect != c.Parent.ScissorRect.Value)
{
SRect = c.Parent.ScissorRect.Value;
StopStart(spriteBatch, SRect, c.SamplerState);
}
c.Draw(spriteBatch);
});
}
finally
{
if (spriteBatch.GraphicsDevice.ScissorRectangle != OriginalSRect) StopStart(spriteBatch, OriginalSRect);
}
RunStraigth(c =>
{
if (!c.Visible || c.CulledOut) return;
c.DrawFront(spriteBatch);
});
sw.Stop();
// CUIDebug.EnsureCategory();
// CUIDebug.CaptureTicks(sw.ElapsedTicks, "CUI.Draw");
}
#endregion
// https://youtu.be/xuFgUmYCS8E?feature=shared&t=72
#region HandleInput Start
public void OnDragEnd(CUIDragHandle h) { if (h == GrabbedDragHandle) GrabbedDragHandle = null; }
public void OnResizeEnd(CUIResizeHandle h) { if (h == GrabbedResizeHandle) GrabbedResizeHandle = null; }
public void OnSwipeEnd(CUISwipeHandle h) { if (h == GrabbedSwipeHandle) GrabbedSwipeHandle = null; }
private void HandleInput(double totalTime)
{
HandleGlobal(totalTime);
HandleMouse(totalTime);
HandleKeyboard(totalTime);
}
private void HandleGlobal(double totalTime)
{
if (CUI.Input.MouseDown) Global.InvokeOnMouseDown(CUI.Input);
if (CUI.Input.MouseUp)
{
Global.InvokeOnMouseUp(CUI.Input);
Global.InvokeOnClick(CUI.Input);
}
if (CUI.Input.MouseMoved) Global.InvokeOnMouseMoved(CUI.Input);
if (CUI.Input.SomeKeyPressed) Global.InvokeOnKeyDown(CUI.Input);
if (CUI.Input.SomeKeyUnpressed) Global.InvokeOnKeyUp(CUI.Input);
}
private void HandleKeyboard(double totalTime)
{
if (FocusedComponent == null) FocusedComponent = this;
if (CUI.Input.PressedKeys.Contains(Keys.Escape)) FocusedComponent = this;
if (CUI.Input.SomeKeyPressed) FocusedComponent.InvokeOnKeyDown(CUI.Input);
if (CUI.Input.SomeKeyUnpressed) FocusedComponent.InvokeOnKeyUp(CUI.Input);
if (CUI.Input.SomeWindowEvents) FocusedComponent.InvokeOnTextInput(CUI.Input);
}
private void HandleMouse(double totalTime)
{
if (!CUI.Input.SomethingHappened) return;
if (!CUI.Input.MouseHeld)
{
GrabbedDragHandle?.EndDrag();
GrabbedResizeHandle?.EndResize();
GrabbedSwipeHandle?.EndSwipe();
}
if (CUI.Input.MouseMoved)
{
GrabbedDragHandle?.DragTo(CUI.Input.MousePosition);
GrabbedResizeHandle?.Resize(CUI.Input.MousePosition);
GrabbedSwipeHandle?.Swipe(CUI.Input);
}
if (CUI.Input.MouseInputHandled) return;
//HACK
//if (CUI.Input.ClickConsumed) return;
//TODO think where should i put it?
if (GrabbedResizeHandle != null || GrabbedDragHandle != null || GrabbedSwipeHandle != null) return;
List<CUIComponent> prevMouseOnList = new List<CUIComponent>(MouseOnList);
CUIComponent CurrentMouseOn = null;
MouseOnList.Clear();
// form MouseOnList
// Note: including main component
if (
GUI.MouseOn == null || (GUI.MouseOn is GUIButton btn && btn.Text == "DUMMY")
|| (this == CUI.TopMain) //TODO guh
)
{
RunStraigth(c =>
{
bool ok = !c.IgnoreEvents && c.Real.Contains(CUI.Input.MousePosition) && c.ShouldInvoke(CUI.Input);
if (c.Parent != null && c.Parent.ScissorRect.HasValue &&
!c.Parent.ScissorRect.Value.Contains(CUI.Input.Mouse.Position))
{
ok = false;
}
if (ok) MouseOnList.Add(c);
});
}
MouseOn = MouseOnList.LastOrDefault();
//HACK
if (MouseOn != this)
{
CUI.Input.MouseInputHandled = true;
CUIMultiModResolver.MarkOtherInputsAsHandled();
}
//if (CurrentMouseOn != null) GUI.MouseOn = dummyComponent;
foreach (CUIComponent c in prevMouseOnList)
{
c.MousePressed = false;
c.MouseOver = false;
c.InvokeOnMouseOff(CUI.Input);
}
foreach (CUIComponent c in MouseOnList)
{
c.MousePressed = CUI.Input.MouseHeld;
c.MouseOver = true;
c.InvokeOnMouseOn(CUI.Input);
}
// Mouse enter / leave
foreach (CUIComponent c in prevMouseOnList.Except(MouseOnList)) c.InvokeOnMouseLeave(CUI.Input);
foreach (CUIComponent c in MouseOnList.Except(prevMouseOnList)) c.InvokeOnMouseEnter(CUI.Input);
// focus
if (CUI.Input.MouseDown)
{
CUIComponent newFocused = this;
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (MouseOnList[i].FocusHandle.ShouldStart(CUI.Input))
{
newFocused = MouseOnList[i];
break;
}
}
FocusedComponent = newFocused;
}
// Resize
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (MouseOnList[i].RightResizeHandle.ShouldStart(CUI.Input))
{
GrabbedResizeHandle = MouseOnList[i].RightResizeHandle;
GrabbedResizeHandle.BeginResize(CUI.Input.MousePosition);
break;
}
if (MouseOnList[i].LeftResizeHandle.ShouldStart(CUI.Input))
{
GrabbedResizeHandle = MouseOnList[i].LeftResizeHandle;
GrabbedResizeHandle.BeginResize(CUI.Input.MousePosition);
break;
}
}
if (GrabbedResizeHandle != null) return;
//Scroll
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (CUI.Input.Scrolled) MouseOnList[i].InvokeOnScroll(CUI.Input);
if (MouseOnList[i].ConsumeMouseScroll) break;
}
//Move
if (CUI.Input.MouseMoved)
{
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
MouseOnList[i].InvokeOnMouseMove(CUI.Input);
}
}
//Clicks
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (CUI.Input.MouseDown) MouseOnList[i].InvokeOnMouseDown(CUI.Input);
if (CUI.Input.MouseUp)
{
MouseOnList[i].InvokeOnMouseUp(CUI.Input);
MouseOnList[i].InvokeOnClick(CUI.Input);
}
if (CUI.Input.DoubleClick) MouseOnList[i].InvokeOnDClick(CUI.Input);
if (MouseOnList[i].ConsumeMouseClicks || CUI.Input.ClickConsumed) break;
}
if (CUI.Input.ClickConsumed) return;
// Swipe
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (MouseOnList[i].SwipeHandle.ShouldStart(CUI.Input))
{
GrabbedSwipeHandle = MouseOnList[i].SwipeHandle;
GrabbedSwipeHandle.BeginSwipe(CUI.Input.MousePosition);
break;
}
if (MouseOnList[i].ConsumeSwipe) break;
}
if (GrabbedSwipeHandle != null) return;
// Drag
for (int i = MouseOnList.Count - 1; i >= 0; i--)
{
if (MouseOnList[i].DragHandle.ShouldStart(CUI.Input))
{
GrabbedDragHandle = MouseOnList[i].DragHandle;
GrabbedDragHandle.BeginDrag(CUI.Input.MousePosition);
break;
}
if (MouseOnList[i].ConsumeDragAndDrop) break;
}
if (GrabbedDragHandle != null) return;
}
#endregion
#region HandleInput End
#endregion
/// <summary>
/// Obsolete function
/// Will run generator func with this
/// </summary>
/// <param name="initFunc"> Generator function that adds components to passed Main </param>
public void Load(Action<CUIMainComponent> initFunc)
{
RemoveAllChildren();
initFunc(this);
}
public CUIMainComponent() : base()
{
CullChildren = true;
Real = new CUIRect(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
Visible = false;
//IgnoreEvents = true;
ShouldPassPropsToChildren = false;
Debug = true;
ChildrenBoundaries = CUIBoundaries.Box;
}
public void PrintRecalLimitWarning()
{
CUI.Log($"Warning: Your GUI code requires {MaxLayoutRecalcLoopsPerUpdate} layout update loops to fully resolve (which is cringe). Optimize it!", Color.Orange);
}
}
}

View File

@@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
namespace QICrabUI
{
/// <summary>
/// Swipable and zoomable plane
/// Allows you to place components in a plane
/// and connect them with lines like a graph or scheme
/// </summary>
public class CUIMap : CUIComponent
{
#region CUIMapLink
#endregion
public class CUIMapLink
{
internal static void InitStatic()
{
CUI.OnInit += () => Default = new CUIMapLink(null, null);
CUI.OnDispose += () => Default = null;
}
public static CUIMapLink Default;
public CUIComponent Start;
public CUIComponent End;
//TODO all this crap wasn't designed for nested AKA
public string StartAKA;
public string EndAKA;
public float LineWidth;
public Color LineColor;
public XElement ToXML()
{
XElement connection = new XElement("Connection");
if (LineWidth != Default.LineWidth)
{
connection.SetAttributeValue("LineWidth", LineWidth);
}
connection.SetAttributeValue("Start", StartAKA ?? "");
connection.SetAttributeValue("End", EndAKA ?? "");
return connection;
}
public CUIMapLink(CUIComponent start, CUIComponent end, Color? lineColor = null, float lineWidth = 2f)
{
LineColor = lineColor ?? new Color(128, 128, 128);
LineWidth = lineWidth;
Start = start;
End = end;
StartAKA = start?.AKA;
EndAKA = end?.AKA;
}
}
#region LinksContainer
#endregion
public class LinksContainer : CUIComponent
{
public List<CUIMapLink> Connections = new List<CUIMapLink>();
public override void Draw(SpriteBatch spriteBatch)
{
base.Draw(spriteBatch);
foreach (CUIMapLink link in Connections)
{
Vector2 midPoint = new Vector2(link.End.Real.Center.X, link.Start.Real.Center.Y);
GUI.DrawLine(spriteBatch,
link.Start.Real.Center,
midPoint,
link.LineColor, width: link.LineWidth
);
GUI.DrawLine(spriteBatch,
midPoint,
link.End.Real.Center,
link.LineColor, width: link.LineWidth
);
}
}
public LinksContainer()
{
UnCullable = true;
BackgroundColor = Color.Transparent;
Border.Color = Color.Transparent;
}
}
#region CUIMap
#endregion
public LinksContainer linksContainer;
public List<CUIMapLink> Connections => linksContainer.Connections;
public CUIComponent Add(CUIComponent c) => Append(c, c.AKA);
public CUIComponent Connect(CUIComponent startComponent, CUIComponent endComponent, Color? color = null)
{
if (startComponent != null && endComponent != null)
{
if (color == null && (!startComponent.Disabled || !endComponent.Disabled)) color = new Color(0, 0, 255);
linksContainer.Connections.Add(new CUIMapLink(startComponent, endComponent, color));
}
return startComponent;
}
public CUIComponent Connect(CUIComponent startComponent, int end = -2, Color? color = null)
{
end = MathUtils.PositiveModulo(end, Children.Count);
CUIComponent endComponent = Children.ElementAtOrDefault(end);
return Connect(startComponent, endComponent, color);
}
//TODO DRY
public CUIComponent Connect(string start, string end, Color? color = null)
{
CUIComponent startComponent = this[start];
CUIComponent endComponent = this[end];
if (startComponent != null && endComponent != null)
{
if (color == null && (!startComponent.Disabled || !endComponent.Disabled)) color = new Color(0, 0, 255);
linksContainer.Connections.Add(new CUIMapLink(startComponent, endComponent, color)
{
StartAKA = start,
EndAKA = end,
});
}
return startComponent;
}
public CUIComponent Connect(int start, int end, Color? color = null)
{
start = MathUtils.PositiveModulo(start, Children.Count);
end = MathUtils.PositiveModulo(end, Children.Count);
CUIComponent startComponent = Children.ElementAtOrDefault(start);
CUIComponent endComponent = Children.ElementAtOrDefault(end);
return Connect(startComponent, endComponent, color);
}
public CUIComponent ConnectTo(CUIComponent Host, params CUIComponent[] children)
{
foreach (CUIComponent child in children) { Connect(Host, child); }
return Host;
}
public override XElement ToXML(CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
Type type = GetType();
XElement element = new XElement(type.Name);
PackProps(element, propAttribute);
XElement connections = new XElement("Connections");
element.Add(connections);
foreach (CUIMapLink link in Connections)
{
connections.Add(link.ToXML());
}
XElement children = new XElement("Children");
element.Add(children);
foreach (CUIComponent child in Children)
{
if (child == linksContainer) continue;
children.Add(child.ToXML());
}
return element;
}
public override void FromXML(XElement element, string baseFolder = null)
{
foreach (XElement childElement in element.Element("Children").Elements())
{
Type childType = CUIReflection.GetComponentTypeByName(childElement.Name.ToString());
if (childType == null) continue;
CUIComponent child = (CUIComponent)Activator.CreateInstance(childType);
child.FromXML(childElement);
this.Append(child, child.AKA);
}
foreach (XElement link in element.Element("Connections").Elements())
{
CUIComponent startComponent = this[link.Attribute("Start").Value];
CUIComponent endComponent = this[link.Attribute("End").Value];
if (startComponent == null || endComponent == null)
{
CUIDebug.Error("startComponent == null || endComponent == null");
continue;
}
Connect(link.Attribute("Start").Value, link.Attribute("End").Value);
}
//TODO: think, this is potentially very bugged,
// Some props might need to be assigned before children, and some after
ExtractProps(element);
}
public CUIMap() : base()
{
Swipeable = true;
ConsumeMouseClicks = true;
CullChildren = true;
BackgroundColor = Color.Transparent;
//without container links won't be culled
//TODO linksContainer should be special and not just first child
this["links"] = linksContainer = new LinksContainer();
OnScroll += (m) =>
{
CUIProps.ChildrenOffset.SetValue(
ChildrenOffset.Zoom(
m.MousePosition - Real.Position,
(-m.Scroll / 500f)
)
);
};
}
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Button with multiple options
/// which are rotating when you click
/// </summary>
public class CUIMultiButton : CUIButton
{
private List<string> options = new List<string>();
/// <summary>
/// Options are just strings
/// </summary>
[CUISerializable]
public IEnumerable<string> Options
{
get => options;
set => options = value.ToList();
}
public event Action<string> OnSelect;
public Action<string> AddOnSelect { set { OnSelect += value; } }
public bool CycleOnClick { get; set; } = true;
public int SelectedIndex
{
get => options.IndexOf(Selected);
set
{
if (options.Count == 0)
{
Selected = "";
}
else
{
Selected = options.ElementAtOrDefault(value % options.Count) ?? "";
}
}
}
[CUISerializable]
public string Selected
{
get => Text;
set
{
Text = value;
OnSelect?.Invoke(value);
}
}
public void Add(string option) => options.Add(option);
public void Remove(string option)
{
int i = options.IndexOf(option);
options.Remove(option);
if (option == Selected) Select(i);
}
public void Select(int i) => SelectedIndex = i;
public void Select(string option) => Selected = option;
public void SelectNext() => SelectedIndex++;
public void SelectPrev() => SelectedIndex--;
public CUIMultiButton() : base()
{
Text = "MultiButton";
OnMouseDown += (e) =>
{
if (CycleOnClick)
{
SelectNext();
if (Command != null) DispatchUp(new CUICommand(Command, Selected));
}
};
}
/// <summary>
/// CUITextBlock DoWrapFor but for all text
/// </summary>
/// <param name="size"></param>
/// <returns></returns>
protected override Vector2 DoWrapFor(Vector2 size)
{
if ((!WrappedForThisSize.HasValue || size == WrappedForThisSize.Value) && !TextPropChanged) return WrappedSize;
TextPropChanged = false;
WrappedForThisSize = size;
if (Vertical) size = new Vector2(0, size.Y);
IEnumerable<string> WrappedTexts;
if (Wrap || Vertical)
{
WrappedText = Font.WrapText(Text, size.X / TextScale - Padding.X * 2).Trim('\n');
WrappedTexts = options.Select(o => Font.WrapText(o, size.X / TextScale - Padding.X * 2).Trim('\n'));
}
else
{
WrappedText = Text;
WrappedTexts = options;
}
IEnumerable<Vector2> RealTextSizes = WrappedTexts.Select(t => Font.MeasureString(t) * TextScale);
float maxX = 0;
float maxY = 0;
foreach (Vector2 s in RealTextSizes)
{
if (s.X > maxX) maxX = s.X;
if (s.Y > maxY) maxY = s.Y;
}
Vector2 MaxTextSize = new Vector2(maxX, maxY);
RealTextSize = Font.MeasureString(WrappedText) * TextScale;
if (WrappedText == "") RealTextSize = new Vector2(0, 0);
RealTextSize = new Vector2((float)Math.Round(RealTextSize.X), (float)Math.Round(RealTextSize.Y));
Vector2 minSize = MaxTextSize + Padding * 2;
if (!Wrap || Vertical)
{
SetForcedMinSize(new CUINullVector2(minSize));
}
WrappedSize = new Vector2(Math.Max(size.X, minSize.X), Math.Max(size.Y, minSize.Y));
return WrappedSize;
}
internal override Vector2 AmIOkWithThisSize(Vector2 size)
{
return DoWrapFor(size);
}
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Container for other components
/// Can have only 1 child
/// Sets component as it's only child when you open it (as a page)
/// </summary>
public class CUIPages : CUIComponent
{
public CUIComponent OpenedPage;
public bool IsOpened(CUIComponent p) => OpenedPage == p;
/// <summary>
/// Adds page as its only child
/// </summary>
/// <param name="page"></param>
public void Open(CUIComponent page)
{
RemoveAllChildren();
Append(page);
page.Relative = new CUINullRect(0, 0, 1, 1);
OpenedPage = page;
}
public CUIPages() : base()
{
BackgroundColor = Color.Transparent;
Border.Color = Color.Transparent;
CullChildren = false;
}
}
}

View File

@@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml.Linq;
namespace QICrabUI
{
/// <summary>
/// Passive block of text
/// </summary>
public class CUITextBlock : CUIComponent
{
public event Action OnTextChanged;
public Action AddOnTextChanged { set { OnTextChanged += value; } }
//TODO move padding here, it makes no sense in CUIComponent
private bool wrap;
[CUISerializable]
public bool Wrap
{
get => wrap;
set
{
wrap = value;
MeasureUnwrapped();
TextPropChanged = true;
}
}
[CUISerializable] public Color TextColor { get; set; }
private GUIFont font = GUIStyle.Font;
[CUISerializable]
public GUIFont Font
{
get => font;
set
{
font = value;
MeasureUnwrapped();
TextPropChanged = true;
}
}
/// <summary>
/// A Vector2 ([0..1],[0..1])
/// </summary>
[CUISerializable] public Vector2 TextAlign { get; set; }
[CUISerializable] public bool Vertical { get; set; }
/// <summary>
/// Lil optimization: ghost text won't set forsed size and parent won't be able to fit to it
/// But it will increase performance in large lists
/// </summary>
[CUISerializable] public bool GhostText { get; set; }
[CUISerializable]
public string Text { get => text; set => SetText(value); }
[CUISerializable]
public float TextScale { get => textScale; set => SetTextScale(value); }
#region Cringe
protected Vector2 RealTextSize;
[Calculated] protected Vector2 TextDrawPos { get; set; }
[Calculated] protected string WrappedText { get; set; } = "";
protected Vector2? WrappedForThisSize;
[Calculated] protected Vector2 WrappedSize { get; set; }
public Vector2 UnwrappedTextSize { get; set; }
public Vector2 UnwrappedMinSize { get; set; }
protected bool TextPropChanged;
#endregion
protected string text = ""; internal void SetText(string value)
{
text = value ?? "";
OnTextChanged?.Invoke();
MeasureUnwrapped();
TextPropChanged = true;
OnPropChanged();
OnAbsolutePropChanged();
}
protected float textScale = 0.9f; internal void SetTextScale(float value)
{
textScale = value;
MeasureUnwrapped();
TextPropChanged = true;
OnPropChanged();
OnAbsolutePropChanged();
}
//Note: works only on unwrapped text for now because WrappedText is delayed
/// <summary>
/// X coordinate of caret if there was one
/// Used in CUITextInput, you don't need this
/// </summary>
/// <param name="i"></param>
/// <returns></returns>
public float CaretPos(int i)
{
return Font.MeasureString(Text.SubstringSafe(0, i)).X * TextScale + Padding.X;
}
//Note: works only on unwrapped text for now because WrappedText is delayed
/// <summary>
/// Tndex of caret if there was one
/// Used in CUITextInput, you don't need this
/// </summary>
/// <param name="i"></param>
/// <returns></returns>
public int CaretIndex(float x)
{
int Aprox = (int)Math.Round((x - Padding.X) / Font.MeasureString(Text).X * Text.Length);
int closestCaretPos = Aprox;
float smallestDif = Math.Abs(x - CaretPos(Aprox));
for (int i = Aprox - 2; i <= Aprox + 2; i++)
{
float dif = Math.Abs(x - CaretPos(i));
if (dif < smallestDif)
{
closestCaretPos = i;
smallestDif = dif;
}
}
return closestCaretPos;
}
// Small optimisation, doesn't seem to save much
protected virtual void MeasureUnwrapped()
{
UnwrappedTextSize = Font.MeasureString(Text) * TextScale;
UnwrappedMinSize = UnwrappedTextSize + Padding * 2;
}
protected virtual Vector2 DoWrapFor(Vector2 size)
{
// To prevent loop
if (!(WrappedForThisSize.HasValue && WrappedForThisSize != size) && !TextPropChanged) return WrappedSize;
TextPropChanged = false;
WrappedForThisSize = size;
// There's no way to wrap vertical text
bool isInWrapZone = Vertical ? false : size.X <= UnwrappedMinSize.X;
bool isSolid = Vertical || !Wrap;
if (Vertical) size = new Vector2(0, size.Y);
if ((Wrap && isInWrapZone) || Vertical)
{
WrappedText = Font.WrapText(Text, size.X / TextScale - Padding.X * 2).Trim('\n');
RealTextSize = Font.MeasureString(WrappedText) * TextScale;
}
else
{
WrappedText = Text;
RealTextSize = UnwrappedTextSize;
}
if (WrappedText == "") RealTextSize = new Vector2(0, 0);
RealTextSize = new Vector2((float)Math.Round(RealTextSize.X), (float)Math.Round(RealTextSize.Y));
Vector2 minSize = RealTextSize + Padding * 2;
if (isSolid && !GhostText)
{
SetForcedMinSize(new CUINullVector2(minSize));
}
WrappedSize = new Vector2(Math.Max(size.X, minSize.X), Math.Max(size.Y, minSize.Y));
return WrappedSize;
}
internal override Vector2 AmIOkWithThisSize(Vector2 size)
{
return DoWrapFor(size);
}
//Note: This is a bottleneck for large lists of text
internal override void UpdatePseudoChildren()
{
if (CulledOut) return;
TextDrawPos = CUIAnchor.GetChildPos(Real, TextAlign, Vector2.Zero, RealTextSize / Scale) + Padding * CUIAnchor.Direction(TextAlign) / Scale;
//CUIDebug.Capture(null, this, "UpdatePseudoChildren", "", "TextDrawPos", $"{TextDrawPos - Real.Position}");
}
public override void Draw(SpriteBatch spriteBatch)
{
base.Draw(spriteBatch);
// Font.DrawString(spriteBatch, WrappedText, TextDrawPos, TextColor, rotation: 0, origin: Vector2.Zero, TextScale, spriteEffects: SpriteEffects.None, layerDepth: 0.1f);
Font.Value.DrawString(spriteBatch, WrappedText, TextDrawPos, TextColor, rotation: 0, origin: Vector2.Zero, TextScale / Scale, se: SpriteEffects.None, layerDepth: 0.1f);
}
public CUITextBlock() { }
public CUITextBlock(string text) : this()
{
Text = text;
}
}
}

View File

@@ -0,0 +1,479 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using EventInput;
using System.Windows;
namespace QICrabUI
{
/// <summary>
/// Text input
/// </summary>
public class CUITextInput : CUIComponent, IKeyboardSubscriber
{
#region IKeyboardSubscriber
private Keys pressedKey;
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveSpecialInput(Keys key)
{
try
{
pressedKey = key;
if (key == Keys.Back) Back();
if (key == Keys.Delete) Delete();
if (key == Keys.Left) MoveLeft();
if (key == Keys.Right) MoveRight();
}
catch (Exception e)
{
CUI.Warning(e);
}
}
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveTextInput(char inputChar) => ReceiveTextInput(inputChar.ToString());
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveTextInput(string text)
{
try
{
CutSelection();
Text = Text.Insert(CaretPos, text);
CaretPos = CaretPos + 1;
OnTextAdded?.Invoke(text);
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
//CUI.Log($"ReceiveTextInput {text}");
}
catch (Exception e)
{
CUI.Log(e);
}
}
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveCommandInput(char command)
{
try
{
if (pressedKey == Keys.A) SelectAll();
if (pressedKey == Keys.C) Copy();
if (pressedKey == Keys.V) Paste();
}
catch (Exception e)
{
CUI.Warning(e);
}
//CUI.Log($"ReceiveCommandInput {command}");
}
//Alt+tab?
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveEditingInput(string text, int start, int length)
{
//CUI.Log($"ReceiveEditingInput {text} {start} {length}");
}
//TODO mb should lose focus here
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public bool Selected { get; set; }
#endregion
#region Commands
public void SelectAll() => Select(0, Text.Length);
public void Copy()
{
if (Selection.IsEmpty) return;
selectionHandle.Grabbed = false;
Clipboard.SetText(Text.SubstringSafe(Selection.Start, Selection.End));
}
public void Paste()
{
ReceiveTextInput(Clipboard.GetText());
}
public void AddText(string text) => ReceiveTextInput(text);
public void MoveLeft()
{
CaretPos--;
Selection = IntRange.Zero;
}
public void MoveRight()
{
CaretPos++;
Selection = IntRange.Zero;
}
// //TODO
// public void SelectLeft()
// {
// if (Selection == IntRange.Zero) Selection = new IntRange(CaretPos - 1, CaretPos);
// else Selection = new IntRange(Selection.Start - 1, Selection.End);
// }
// //TODO
// public void SelectRight()
// {
// if (Selection == IntRange.Zero) Selection = new IntRange(CaretPos, CaretPos + 1);
// else Selection = new IntRange(Selection.Start, Selection.End + 1);
// }
public void Back()
{
TextInputState oldState = State;
if (!Selection.IsEmpty) CutSelection();
else
{
string s1 = oldState.Text.SubstringSafe(0, oldState.CaretPos - 1);
string s2 = oldState.Text.SubstringSafe(oldState.CaretPos);
Text = s1 + s2;
CaretPos = oldState.CaretPos - 1;
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
}
}
public void Delete()
{
TextInputState oldState = State;
if (!Selection.IsEmpty) CutSelection();
else
{
string s1 = oldState.Text.SubstringSafe(0, oldState.CaretPos);
string s2 = oldState.Text.SubstringSafe(oldState.CaretPos + 1);
Text = s1 + s2;
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
//CaretPos = oldState.CaretPos;
}
}
public void CutSelection()
{
if (Selection.IsEmpty) return;
selectionHandle.Grabbed = false;
string s1 = Text.SubstringSafe(0, Selection.Start);
string s2 = Text.SubstringSafe(Selection.End);
Text = s1 + s2;
CaretPos = Selection.Start;
Selection = IntRange.Zero;
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
}
internal int SetCaretPos(Vector2 v)
{
int newCaretPos = TextComponent.CaretIndex(v.X);
CaretPos = newCaretPos;
return newCaretPos;
}
#endregion
internal class SelectionHandle
{
public bool Grabbed;
public int lastSelectedPos;
}
internal SelectionHandle selectionHandle = new SelectionHandle();
internal record struct TextInputState(string Text, IntRange Selection, int CaretPos)
{
public string Text { get; init; } = Text ?? "";
}
private TextInputState state; internal TextInputState State
{
get => state;
set
{
state = ValidateState(value);
ApplyState(state);
}
}
internal TextInputState ValidateState(TextInputState state)
{
//return state with { CaretPos = state.CaretPos.Fit(0, state.Text.Length - 1) };
string newText = state.Text;
IntRange newSelection = new IntRange(
state.Selection.Start.Fit(0, newText.Length),
state.Selection.End.Fit(0, newText.Length)
);
int newCaretPos = state.CaretPos.Fit(0, newText.Length);
return new TextInputState(newText, newSelection, newCaretPos);
}
internal void ApplyState(TextInputState state)
{
TextComponent.Text = state.Text;
SelectionOverlay.Visible = !state.Selection.IsEmpty;
CaretIndicatorVisible = Focused && !SelectionOverlay.Visible;
if (!state.Selection.IsEmpty)
{
SelectionOverlay.Absolute = SelectionOverlay.Absolute with
{
Left = TextComponent.CaretPos(state.Selection.Start),
Width = TextComponent.CaretPos(state.Selection.End) - TextComponent.CaretPos(state.Selection.Start),
};
}
CaretIndicator.Absolute = CaretIndicator.Absolute with
{
Left = TextComponent.CaretPos(state.CaretPos),
};
}
private bool valid = true; public bool Valid
{
get => valid;
set
{
if (valid == value) return;
valid = value;
UpdateBorderColor();
}
}
public Type VatidationType { get; set; }
public bool IsValidText(string text)
{
if (VatidationType == null) return true;
if (VatidationType == typeof(string)) return true;
if (VatidationType == typeof(Color)) return true;
if (VatidationType == typeof(bool)) return bool.TryParse(text, out _);
if (VatidationType == typeof(int)) return int.TryParse(text, out _);
if (VatidationType == typeof(float)) return float.TryParse(text, out _);
if (VatidationType == typeof(double)) return double.TryParse(text, out _);
return false;
}
//TODO this is cringe
// public override void Consume(object o)
// {
// string value = (string)o;
// State = new TextInputState(value, State.Selection, State.CaretPos);
// Valid = IsValidText(value);
// }
internal CUITextBlock TextComponent;
public string Text
{
get => State.Text;
set
{
if (Disabled) return;
State = new TextInputState(value, State.Selection, State.CaretPos);
bool isvalid = IsValidText(value);
if (isvalid)
{
OnTextChanged?.Invoke(State.Text);
}
Valid = isvalid;
}
}
internal CUIComponent SelectionOverlay;
public IntRange Selection
{
get => State.Selection;
set => State = new TextInputState(State.Text, value, State.CaretPos);
}
public void Select(int start, int end) => Selection = new IntRange(start, end);
public bool CaretIndicatorVisible { get; set; }
public double CaretBlinkInterval { get; set; } = 0.5;
internal CUIComponent CaretIndicator;
public int CaretPos
{
get => State.CaretPos;
set => State = new TextInputState(State.Text, State.Selection, value);
}
//TODO
//[CUISerializable] public bool PreventOverflow { get; set; } = false;
public void UpdateBorderColor()
{
if (Valid)
{
if (Focused)
{
Style["Border"] = "CUIPalette.Input.Focused";
}
else
{
Style["Border"] = "CUIPalette.Input.Border";
}
}
else
{
Style["Border"] = "CUIPalette.Input.Invalid";
}
}
[CUISerializable]
public float TextScale
{
get => TextComponent?.TextScale ?? 0;
set => TextComponent.TextScale = value;
}
public Color TextColor
{
get => TextComponent?.TextColor ?? Color.White;
set
{
if (TextComponent != null)
{
TextComponent.TextColor = value;
}
}
}
public event Action<string> OnTextChanged;
public Action<string> AddOnTextChanged { set => OnTextChanged += value; }
public event Action<string> OnTextAdded;
public Action<string> AddOnTextAdded { set => OnTextAdded += value; }
public override void Draw(SpriteBatch spriteBatch)
{
if (Focused)
{
CaretIndicator.Visible = CaretIndicatorVisible && Timing.TotalTime % CaretBlinkInterval > CaretBlinkInterval / 2;
}
base.Draw(spriteBatch);
}
public CUITextInput(string text) : this()
{
Text = text;
}
public CUITextInput() : base()
{
AbsoluteMin = new CUINullRect(w: 50, h: 22);
FitContent = new CUIBool2(true, true);
Focusable = true;
Border.Thickness = 2;
HideChildrenOutsideFrame = true;
ConsumeMouseClicks = true;
ConsumeDragAndDrop = true;
ConsumeSwipe = true;
BreakSerialization = true;
this["TextComponent"] = TextComponent = new CUITextBlock()
{
Text = "",
Relative = new CUINullRect(0, 0, 1, 1),
TextAlign = CUIAnchor.CenterLeft,
Style = new CUIStyle(){
{"Padding", "[2,2]"},
{"TextColor", "CUIPalette.Input.Text"},
},
};
this["SelectionOverlay"] = SelectionOverlay = new CUIComponent()
{
Style = new CUIStyle(){
{"BackgroundColor", "CUIPalette.Input.Selection"},
{"Border", "Transparent"},
},
Relative = new CUINullRect(h: 1),
Ghost = new CUIBool2(true, true),
IgnoreParentVisibility = true,
};
this["CaretIndicator"] = CaretIndicator = new CUIComponent()
{
Style = new CUIStyle(){
{"BackgroundColor", "CUIPalette.Input.Caret"},
{"Border", "Transparent"},
},
Relative = new CUINullRect(y: 0.1f, h: 0.8f),
Absolute = new CUINullRect(w: 1),
Ghost = new CUIBool2(true, true),
Visible = false,
IgnoreParentVisibility = true,
};
OnConsume += (o) =>
{
string value = o.ToString();
State = new TextInputState(value, State.Selection, State.CaretPos);
Valid = IsValidText(value);
};
OnFocus += () =>
{
UpdateBorderColor();
CaretIndicator.Visible = true;
};
OnFocusLost += () =>
{
UpdateBorderColor();
Selection = IntRange.Zero;
CaretIndicator.Visible = false;
};
OnMouseDown += (e) =>
{
int newCaretPos = SetCaretPos(e.MousePosition - Real.Position);
Selection = IntRange.Zero;
selectionHandle.lastSelectedPos = newCaretPos;
selectionHandle.Grabbed = true;
};
OnMouseMove += (e) =>
{
if (selectionHandle.Grabbed)
{
int nextCaretPos = SetCaretPos(e.MousePosition - Real.Position);
Selection = new IntRange(selectionHandle.lastSelectedPos, nextCaretPos);
}
};
OnDClick += (e) => SelectAll();
if (CUI.Main is not null)
{
CUI.Main.Global.OnMouseUp += (e) => selectionHandle.Grabbed = false;
}
}
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using Barotrauma.Extensions;
namespace QICrabUI
{
/// <summary>
/// A button which can be on and off
/// It's derived from CUITextBlock and has all its props
/// </summary>
public class CUIToggleButton : CUITextBlock
{
[CUISerializable]
public GUISoundType ClickSound { get; set; } = GUISoundType.Select;
[CUISerializable] public Color DisabledColor { get; set; }
[CUISerializable] public Color OnColor { get; set; }
[CUISerializable] public Color OnHoverColor { get; set; }
[CUISerializable] public Color OffColor { get; set; }
[CUISerializable] public Color OffHoverColor { get; set; }
public Color MasterColor
{
set
{
OffColor = value.Multiply(0.5f);
OffHoverColor = value;
OnColor = value.Multiply(0.9f);
OnHoverColor = value;
}
}
public Color MasterColorOpaque
{
set
{
OffColor = new Color((int)(value.R * 0.5f), (int)(value.G * 0.5f), (int)(value.B * 0.5f), value.A);
OffHoverColor = value;
OnColor = new Color((int)(value.R * 0.9f), (int)(value.G * 0.9f), (int)(value.B * 0.9f), value.A); ;
OnHoverColor = value;
}
}
// BackgroundColor is used in base.Draw, but here it's calculated from OnColor/OffColor
// so it's not a prop anymore, and i don't want to serialize it
public new Color BackgroundColor { get => CUIProps.BackgroundColor.Value; set => CUIProps.BackgroundColor.SetValue(value); }
private string onText;
private string offText;
[CUISerializable]
public string OnText
{
get => onText;
set { onText = value; if (State && onText != null) Text = onText; }
}
[CUISerializable]
public string OffText
{
get => offText;
set { offText = value; if (!State && offText != null) Text = offText; }
}
public event Action<bool> OnStateChange;
public Action<bool> AddOnStateChange { set { OnStateChange += value; } }
protected bool state;
[CUISerializable]
public bool State
{
get => state;
set
{
state = value;
if (state && OnText != null) Text = OnText;
if (!state && OffText != null) Text = OffText;
}
}
public override void Draw(SpriteBatch spriteBatch)
{
if (Disabled)
{
BackgroundColor = DisabledColor;
}
else
{
if (State)
{
if (MouseOver) BackgroundColor = OnHoverColor;
else BackgroundColor = OnColor;
}
else
{
if (MouseOver) BackgroundColor = OffHoverColor;
else BackgroundColor = OffColor;
}
}
base.Draw(spriteBatch);
}
public CUIToggleButton() : base()
{
ConsumeMouseClicks = true;
ConsumeDragAndDrop = true;
ConsumeSwipe = true;
//BackgroundColor = OffColor;
TextAlign = new Vector2(0.5f, 0.5f);
Padding = new Vector2(4, 2);
Text = nameof(CUIToggleButton);
OnMouseDown += (e) =>
{
if (!Disabled)
{
State = !State;
SoundPlayer.PlayUISound(ClickSound);
OnStateChange?.Invoke(State);
}
};
}
public CUIToggleButton(string text) : this()
{
Text = text;
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Resizing components to it's Width and placing them sequentially
/// </summary>
public class CUIVerticalList : CUIComponent
{
[CUISerializable] public bool Scrollable { get; set; }
[CUISerializable] public float ScrollSpeed { get; set; } = 1.0f;
[CUISerializable] public float TopGap { get; set; } = 0;
[CUISerializable] public float BottomGap { get; set; } = 10f;
public override CUILayout Layout
{
get => layout;
set
{
layout = new CUILayoutVerticalList();
layout.Host = this;
}
}
public CUILayoutVerticalList ListLayout => (CUILayoutVerticalList)Layout;
[CUISerializable]
public CUIDirection Direction
{
get => ListLayout.Direction;
set => ListLayout.Direction = value;
}
//TODO test, sync with hlist
[CUISerializable]
public float Gap
{
get => ListLayout.Gap;
set => ListLayout.Gap = value;
}
[CUISerializable]
public bool ResizeToHostWidth
{
get => ListLayout.ResizeToHostWidth;
set => ListLayout.ResizeToHostWidth = value;
}
public float Scroll
{
get => ChildrenOffset.Y;
set
{
if (!Scrollable) return;
CUIProps.ChildrenOffset.SetValue(
ChildrenOffset with { Y = value }
);
}
}
internal override CUIBoundaries ChildOffsetBounds => new CUIBoundaries(
minX: 0,
maxX: 0,
maxY: TopGap,
minY: Math.Min(Real.Height - ListLayout.TotalHeight - BottomGap, 0)
);
public CUIVerticalList() : base()
{
CullChildren = true;
OnScroll += (m) => Scroll += m.Scroll * ScrollSpeed;
ChildrenBoundaries = CUIBoundaries.VerticalTube;
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Simple dialog box with a message and ok button
/// use public static void Open(string msg) to open it
/// </summary>
public class CUIMessageBox : CUIFrame
{
public static void Open(string msg)
{
CUI.TopMain.Append(new CUIMessageBox(msg));
}
public CUIMessageBox(string msg) : base()
{
Palette = PaletteOrder.Quaternary;
Resizible = false;
Relative = new CUINullRect(0, 0, 0.25f, 0.25f);
Anchor = CUIAnchor.Center;
OutlineThickness = 2;
this["layout"] = new CUIVerticalList()
{
Relative = new CUINullRect(0, 0, 1, 1),
};
this["layout"]["main"] = new CUIVerticalList()
{
FillEmptySpace = new CUIBool2(false, true),
Scrollable = true,
ScrollSpeed = 0.5f,
Style = CUIStylePrefab.Main,
};
this["layout"]["main"]["msg"] = new CUITextBlock(msg)
{
TextScale = 1.2f,
Wrap = true,
Font = GUIStyle.Font,
TextAlign = CUIAnchor.TopCenter,
Style = new CUIStyle()
{
["Padding"] = "[10,10]",
},
};
this["layout"]["ok"] = new CUIButton("Ok")
{
TextScale = 1.0f,
Style = new CUIStyle()
{
["Padding"] = "[10,10]",
},
AddOnMouseDown = (e) => this.RemoveSelf(),
};
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace QICrabUI
{
// hmm, idk if this should be a prefab or component
// it's too small for component
// but in prefab i can't use initializer
public class CUICloseButton : CUIButton
{
public CUICloseButton() : base()
{
Command = "Close";
Text = "";
ZIndex = 10;
BackgroundSprite = CUI.TextureManager.GetCUISprite(3, 1);
Absolute = new CUINullRect(0, 0, 15, 15);
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// It's a debug tool, you can use it with cuimg command, it's very fps comsuming
/// </summary>
[NoDefault]
public class CUIMagnifyingGlass : CUICanvas
{
public static CUIFrame GlassFrame;
public static void AddToggleButton()
{
CUI.TopMain["ToggleMagnifyingGlass"] = new CUIButton("MG")
{
Absolute = new CUINullRect(0, 0, 20, 20),
Anchor = CUIAnchor.CenterLeft,
AddOnMouseDown = (e) => ToggleEquip(),
};
}
public static void ToggleEquip()
{
if (GlassFrame != null)
{
GlassFrame.RemoveSelf();
GlassFrame = null;
}
else
{
GlassFrame = new CUIFrame()
{
ZIndex = 100000,
BackgroundColor = Color.Transparent,
Border = new CUIBorder(Color.Cyan, 5),
Anchor = CUIAnchor.Center,
Absolute = new CUINullRect(w: 200, h: 200),
};
GlassFrame["glass"] = new CUIMagnifyingGlass();
CUI.TopMain["MagnifyingGlass"] = GlassFrame;
}
}
public override void CleanUp()
{
texture.Dispose();
base.CleanUp();
}
Texture2D texture;
Color[] backBuffer;
double lastDrawn;
public override void Draw(SpriteBatch spriteBatch)
{
if (Timing.TotalTime - lastDrawn > 0.05)
{
lastDrawn = Timing.TotalTime;
GameMain.Instance.GraphicsDevice.GetBackBufferData<Color>(backBuffer);
texture.SetData(backBuffer);
texture.GetData<Color>(
0, new Rectangle((int)Real.Left, (int)Real.Top, 40, 40), Data, 0, Data.Length
);
SetData();
}
base.Draw(spriteBatch);
}
public CUIMagnifyingGlass() : base()
{
Size = new Point(40, 40);
SamplerState = CUI.NoSmoothing;
Relative = new CUINullRect(0, 0, 1, 1);
int w = GameMain.Instance.GraphicsDevice.PresentationParameters.BackBufferWidth;
int h = GameMain.Instance.GraphicsDevice.PresentationParameters.BackBufferHeight;
backBuffer = new Color[w * h];
texture = new Texture2D(GameMain.Instance.GraphicsDevice, w, h, false, GameMain.Instance.GraphicsDevice.PresentationParameters.BackBufferFormat);
}
}
}

View File

@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using EventInput;
using System.Windows;
namespace QICrabUI
{
//TODO move all this to defauld styles
/// <summary>
/// CUITextBlock adapted for CUIMenu
/// </summary>
public class CUIMenuText : CUITextBlock
{
public CUIMenuText(string text) : this() => Text = text;
public CUIMenuText() : base()
{
Anchor = CUIAnchor.Center;
TextScale = 1.0f;
ZIndex = 100;
TextColor = Color.Black;
}
}
/// <summary>
/// Component with a sprite that will notify parent CUIMenu when clicked
/// </summary>
public class CUIMenuOption : CUIComponent
{
public GUISoundType ClickSound { get; set; } = GUISoundType.Select;
/// <summary>
/// This is the Value that will be send to CUIMenu on click, and will be passed to OnSelect event
/// </summary>
[CUISerializable] public string Value { get; set; }
/// <summary>
/// Normal background color
/// </summary>
[CUISerializable]
public Color BaseColor
{
get => (Color)Animations["hover"].StartValue;
set
{
Animations["hover"].StartValue = value;
Animations["hover"].ApplyValue();
}
}
/// <summary>
/// Background color when hovered
/// </summary>
[CUISerializable]
public Color HoverColor
{
get => (Color)Animations["hover"].EndValue;
set => Animations["hover"].EndValue = value;
}
public CUIMenuOption()
{
BackgroundColor = new Color(255, 255, 255, 255);
Relative = new CUINullRect(0, 0, 1, 1);
IgnoreTransparent = true;
Command = "CUIMenuOption select";
OnMouseDown += (e) =>
{
SoundPlayer.PlayUISound(ClickSound);
DispatchUp(new CUICommand(Command, Value));
};
Animations["hover"] = new CUIAnimation()
{
StartValue = new Color(255, 255, 255, 255),
EndValue = new Color(255, 255, 255, 255),
Duration = 0.1,
ReverseDuration = 0.3,
Property = "BackgroundColor",
};
OnMouseEnter += (e) => Animations["hover"].Forward();
OnMouseLeave += (e) => Animations["hover"].Back();
}
}
public class CUIMenu : CUIComponent, IKeyboardSubscriber
{
// this allows it to intercept esc key press naturally,
// but it also blocks normal hotkey bindings, so idk
// ----------------- IKeyboardSubscriber -----------------
public void ReceiveSpecialInput(Keys key) { if (key == Keys.Escape) Close(); }
public void ReceiveTextInput(char inputChar) => ReceiveTextInput(inputChar.ToString());
public void ReceiveTextInput(string text) { }
public void ReceiveCommandInput(char command) { }
public void ReceiveEditingInput(string text, int start, int length) { }
public bool Selected { get; set; }
// ----------------- IKeyboardSubscriber -----------------
public static void InitStatic() => CUI.OnDispose += () => Menus.Clear();
/// <summary>
/// All loaded menus are stored here by Name
/// </summary>
public static Dictionary<string, CUIMenu> Menus = new();
/// <summary>
/// Initial fade in animation duration, set to 0 to disable
/// </summary>
[CUISerializable]
public double FadeInDuration
{
get => Animations["fade"].Duration;
set => Animations["fade"].Duration = value;
}
/// <summary>
/// Will be used as key for this menu in CUIMenu.Menus
/// </summary>
[CUISerializable] public string Name { get; set; }
/// <summary>
/// Does nothing, just a prop so you could get author programmatically
/// </summary>
[CUISerializable] public string Author { get; set; }
/// <summary>
/// If true will act as IKeyboardSubscriber. Don't
/// </summary>
[CUISerializable] public bool BlockInput { get; set; }
/// <summary>
/// Happens when some CUIMenuOption is clicked, the value of that option is passed to it
/// </summary>
public event Action<string> OnSelect;
/// <summary>
/// Will add this as a child to host (CUI.Main) and start fadein animation
/// </summary>
public void Open(CUIComponent host = null)
{
if (Parent != null) return;
host ??= CUI.Main;
host.Append(this);
if (BlockInput) CUI.FocusedComponent = this;
Animations["fade"].SetToStart();
Animations["fade"].Forward();
}
public void Close() => RemoveSelf();
public void Toggle(CUIComponent host = null)
{
if (Parent != null) Close();
else Open(host);
}
/// <summary>
/// Loads CUIMenu from a file to CUIMenu.Menus
/// </summary>
public static CUIMenu Load(string path)
{
CUIMenu menu = CUIComponent.LoadFromFile<CUIMenu>(path);
if (menu == null) CUI.Warning($"Couldn't load CUIMenu from {path}");
if (menu?.Name != null) Menus[menu.Name] = menu;
return menu;
}
public CUIMenu() : base()
{
BackgroundColor = new Color(255, 255, 255, 255);
Anchor = CUIAnchor.Center;
Transparency = 0.0f;
AddCommand("CUIMenuOption select", (o) =>
{
if (o is string s) OnSelect?.Invoke(s);
//Close();
});
Animations["fade"] = new CUIAnimation()
{
StartValue = 0.0f,
EndValue = 1.0f,
Duration = 0.2,
Property = "Transparency",
};
if (CUI.Main != null)
{
CUI.Main.Global.OnKeyDown += (e) =>
{
if (e.PressedKeys.Contains(Keys.Escape)) Close();
};
CUI.Main.OnMouseDown += (e) => Close();
}
}
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Unfinished crap, don't use
/// </summary>
public class CUIRadialMenuOption : CUIComponent
{
public GUISoundType ClickSound { get; set; } = GUISoundType.Select;
public Color BaseColor
{
get => (Color)this.Animations["hover"].StartValue;
set => this.Animations["hover"].StartValue = value;
}
public Color Hover
{
get => (Color)this.Animations["hover"].EndValue;
set => this.Animations["hover"].EndValue = value;
}
public float TextRadius { get; set; } = 0.4f;
public void SetRotation(float angle)
{
BackgroundSprite.Offset = new Vector2(0.5f, 0.5f);
BackgroundSprite.Rotation = angle;
this["Text"].Relative = new CUINullRect(
(float)(TextRadius * Math.Cos(angle - Math.PI / 2)),
(float)(TextRadius * Math.Sin(angle - Math.PI / 2))
);
}
public Action Callback;
public CUIRadialMenuOption(string name = "", Action callback = null)
{
IgnoreTransparent = true;
Relative = new CUINullRect(0, 0, 1, 1);
Callback = callback;
OnMouseDown += (e) =>
{
SoundPlayer.PlayUISound(ClickSound);
Callback?.Invoke();
};
this.Animations["hover"] = new CUIAnimation()
{
StartValue = new Color(255, 255, 255, 255),
EndValue = new Color(0, 255, 255, 255),
Property = "BackgroundColor",
Duration = 0.2,
ReverseDuration = 0.3,
};
this.Animations["hover"].ApplyValue();
OnMouseEnter += (e) => Animations["hover"].Forward();
OnMouseLeave += (e) => Animations["hover"].Back();
this["Text"] = new CUITextBlock(name)
{
Anchor = CUIAnchor.Center,
ZIndex = 100,
TextScale = 1.0f,
};
}
}
/// <summary>
/// Unfinished crap, don't use
/// </summary>
public class CUIRadialMenu : CUIComponent
{
public CUIRadialMenuOption OptionTemplate = new();
public Dictionary<string, CUIRadialMenuOption> Options = new();
public CUIRadialMenuOption AddOption(string name, Action callback)
{
CUIRadialMenuOption option = new CUIRadialMenuOption(name, callback);
option.ApplyState(OptionTemplate);
option.Animations["hover"].Interpolate = OptionTemplate.Animations["hover"].Interpolate;
option.Animations["hover"].ApplyValue();
this[name] = Options[name] = option;
option.OnClick += (e) => Close();
CalculateRotations();
return option;
}
public void CalculateRotations()
{
float delta = (float)(Math.PI * 2 / Options.Count);
int i = 0;
foreach (CUIRadialMenuOption option in Options.Values)
{
option.SetRotation(delta * i);
i++;
}
}
public bool IsOpened => Parent != null;
public void Open(CUIComponent host = null)
{
host ??= CUI.Main;
host.Append(this);
Animations["fade"].SetToStart();
Animations["fade"].Forward();
}
public void Close()
{
// BlockChildrenAnimations();
// Animations["fade"].SetToEnd();
// Animations["fade"].Back();
RemoveSelf();
}
public CUIRadialMenu() : base()
{
Anchor = CUIAnchor.Center;
Relative = new CUINullRect(h: 0.8f);
CrossRelative = new CUINullRect(w: 0.8f);
BackgroundColor = new Color(255, 255, 255, 255);
//BackgroundSprite = new CUISprite("RadialMenu.png");
Animations["fade"] = new CUIAnimation()
{
StartValue = 0.0f,
EndValue = 1.0f,
Property = "Transparency",
Duration = 0.2,
ReverseDuration = 0.1,
};
//HACK
Animations["fade"].OnStop += (dir) =>
{
if (dir == CUIDirection.Reverse)
{
RemoveSelf();
}
};
}
}
}

View File

@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Horizontal range input
/// </summary>
public class CUISlider : CUIComponent
{
/// <summary>
/// Happens when handle is dragged, value is [0..1]
/// </summary>
public event Action<float> OnSlide;
public Action<float> AddOnSlide { set { OnSlide += value; } }
public float InOutMult => (Real.Width - Real.Height) / Real.Width;
private float lambda;
private float? pendingLambda;
public float Lambda
{
get => lambda;
set
{
lambda = Math.Clamp(value, 0, 1);
pendingLambda = lambda;
}
}
[CUISerializable] public FloatRange Range { get; set; } = new FloatRange(0, 1);
[CUISerializable] public int? Precision { get; set; } = 2;
/// <summary>
/// The handle
/// </summary>
public CUIComponent Handle;
public CUIComponent LeftEnding;
public CUIComponent RightEnding;
public CUISprite MiddleSprite;
public CUIRect MiddleRect;
public Color MasterColor
{
set
{
if (LeftEnding != null) LeftEnding.BackgroundColor = value;
if (RightEnding != null) RightEnding.BackgroundColor = value;
if (Handle != null) Handle.BackgroundColor = value;
}
}
public override void Draw(SpriteBatch spriteBatch)
{
base.Draw(spriteBatch);
CUI.DrawRectangle(spriteBatch, MiddleRect, LeftEnding.BackgroundColor, MiddleSprite);
}
public CUISlider() : base()
{
ChildrenBoundaries = CUIBoundaries.Box;
BreakSerialization = true;
this["LeftEnding"] = LeftEnding = new CUIComponent()
{
Anchor = CUIAnchor.CenterLeft,
Relative = new CUINullRect(h: 1),
CrossRelative = new CUINullRect(w: 1),
BackgroundSprite = CUI.TextureManager.GetCUISprite(2, 2, CUISpriteDrawMode.Resize, SpriteEffects.FlipHorizontally),
Style = new CUIStyle()
{
["Border"] = "Transparent",
["BackgroundColor"] = "CUIPalette.Slider",
},
};
this["RightEnding"] = RightEnding = new CUIComponent()
{
Anchor = CUIAnchor.CenterRight,
Relative = new CUINullRect(h: 1),
CrossRelative = new CUINullRect(w: 1),
BackgroundSprite = CUI.TextureManager.GetCUISprite(2, 2),
Style = new CUIStyle()
{
["Border"] = "Transparent",
["BackgroundColor"] = "CUIPalette.Slider",
},
};
this["handle"] = Handle = new CUIComponent()
{
Style = new CUIStyle()
{
["Border"] = "Transparent",
["BackgroundColor"] = "CUIPalette.Slider",
},
Draggable = true,
BackgroundSprite = CUI.TextureManager.GetCUISprite(0, 2),
Relative = new CUINullRect(h: 1),
CrossRelative = new CUINullRect(w: 1),
AddOnDrag = (x, y) =>
{
lambda = Math.Clamp(x / InOutMult, 0, 1);
OnSlide?.Invoke(lambda);
if (Command != null)
{
float value = Range.PosOf(lambda);
if (Precision.HasValue) value = (float)Math.Round(value, Precision.Value);
DispatchUp(new CUICommand(Command, value));
}
},
};
Handle.DragHandle.DragRelative = true;
MiddleSprite = CUI.TextureManager.GetSprite("CUI.png", new Rectangle(44, 64, 6, 32));
OnLayoutUpdated += () =>
{
MiddleRect = new CUIRect(
Real.Left + Real.Height,
Real.Top,
Real.Width - 2 * Real.Height,
Real.Height
);
if (pendingLambda.HasValue)
{
Handle.Relative = Handle.Relative with
{
Left = Math.Clamp(pendingLambda.Value, 0, 1) * InOutMult,
};
pendingLambda = null;
}
};
OnConsume += (o) =>
{
if (float.TryParse(o.ToString(), out float value))
{
Lambda = Range.LambdaOf(value);
}
};
}
}
}

View File

@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Just a tick box
/// </summary>
public class CUITickBox : CUIComponent
{
public GUISoundType ClickSound { get; set; } = GUISoundType.TickBox;
public event Action<bool> OnStateChange;
public void AddOnStateChange(Action<bool> callback) => OnStateChange += callback;
public CUISprite OnSprite { get; set; }
public CUISprite OffSprite { get; set; }
public CUISprite HoverOffSprite { get; set; }
public CUISprite HoverOnSprite { get; set; }
public CUISprite DisabledSprite { get; set; }
private bool state; public bool State
{
get => state;
set
{
if (state == value) return;
state = value;
ChangeSprite();
}
}
public override bool Disabled
{
get => disabled;
set
{
disabled = value;
ChangeSprite();
}
}
public virtual void ChangeSprite()
{
if (Disabled)
{
BackgroundSprite = DisabledSprite;
return;
}
if (State)
{
BackgroundSprite = OnSprite;
if (MouseOver) BackgroundSprite = HoverOnSprite;
}
else
{
BackgroundSprite = OffSprite;
if (MouseOver) BackgroundSprite = HoverOffSprite;
}
}
public CUITickBox() : base()
{
Absolute = new CUINullRect(w: 20, h: 20);
BackgroundColor = Color.Cyan;
Border.Color = Color.Transparent;
ConsumeMouseClicks = true;
ConsumeDragAndDrop = true;
ConsumeSwipe = true;
OffSprite = new CUISprite(CUI.CUITexturePath)
{
SourceRect = new Rectangle(0, 0, 32, 32),
};
OnSprite = new CUISprite(CUI.CUITexturePath)
{
SourceRect = new Rectangle(32, 0, 32, 32),
};
HoverOffSprite = new CUISprite(CUI.CUITexturePath)
{
SourceRect = new Rectangle(64, 0, 32, 32),
};
HoverOnSprite = new CUISprite(CUI.CUITexturePath)
{
SourceRect = new Rectangle(96, 0, 32, 32),
};
DisabledSprite = new CUISprite(CUI.CUITexturePath)
{
SourceRect = new Rectangle(128, 0, 32, 32),
};
ChangeSprite();
OnMouseDown += (e) =>
{
if (Disabled) return;
SoundPlayer.PlayUISound(ClickSound);
State = !State;
OnStateChange?.Invoke(State);
if (Command != null) DispatchUp(new CUICommand(Command, State));
};
OnMouseEnter += (e) => ChangeSprite();
OnMouseLeave += (e) => ChangeSprite();
OnConsume += (o) =>
{
if (o is bool b) State = b;
};
}
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
namespace QICrabUI
{
/// <summary>
/// Just an example of what CUICanvas can be used for
/// </summary>
public class CUIWater : CUICanvas
{
public float Omega = 1.999f;
public float[,] Pool1;
public float[,] Pool2;
public float[,] DensityMap;
public Color[] ColorPalette = new Color[]{
new Color(0,0,0,0),
new Color(0,0,64),
new Color(32,0,64),
new Color(255,0,255),
new Color(0,255,255),
};
public override Point Size
{
get => base.Size;
set
{
base.Size = value;
Pool1 = new float[Texture.Width, Texture.Height];
Pool2 = new float[Texture.Width, Texture.Height];
DensityMap = new float[Texture.Width, Texture.Height];
RandomizeDensityMap();
}
}
public float NextAmplitude(int x, int y)
{
float avg = (
Pool1[x + 1, y] +
Pool1[x, y + 1] +
Pool1[x - 1, y] +
Pool1[x, y - 1]
) / 4.0f;
return avg * Omega + (1 - Omega) * Pool2[x, y];
}
public void Step()
{
for (int x = 1; x < Size.X - 1; x++)
{
for (int y = 1; y < Size.Y - 1; y++)
{
Pool2[x, y] = NextAmplitude(x, y) * DensityMap[x, y];
}
}
(Pool1, Pool2) = (Pool2, Pool1);
}
public double UpdateInterval = 1.0 / 60.0;
private double lastUpdateTime = -1;
public void Update(double totalTime)
{
if (totalTime - lastUpdateTime < UpdateInterval) return;
UpdateSelf();
Step();
lastUpdateTime = totalTime;
TransferData();
}
public virtual void UpdateSelf()
{
}
private void TransferData()
{
for (int x = 0; x < Size.X; x++)
{
for (int y = 0; y < Size.Y; y++)
{
SetPixel(x, y, ToolBox.GradientLerp(Math.Abs(Pool1[x, y]), ColorPalette));
}
}
SetData();
}
public void RandomizeDensityMap()
{
for (int x = 0; x < Size.X; x++)
{
for (int y = 0; y < Size.Y; y++)
{
DensityMap[x, y] = 1.0f - CUI.Random.NextSingle() * 0.01f;
}
}
}
public float DropSize = 16.0f;
public void Drop(float x, float y)
{
int i = (int)Math.Clamp(Math.Round(x * Texture.Width), 1, Texture.Width - 2);
int j = (int)Math.Clamp(Math.Round(y * Texture.Height), 1, Texture.Height - 2);
Pool1[i, j] = DropSize;
}
public CUIWater(int x, int y) : base(x, y)
{
//ConsumeDragAndDrop = true;
//OnUpdate += Update;
Pool1 = new float[Texture.Width, Texture.Height];
Pool2 = new float[Texture.Width, Texture.Height];
DensityMap = new float[Texture.Width, Texture.Height];
RandomizeDensityMap();
// OnMouseOn += (e) =>
// {
// if (!MousePressed) return;
// Vector2 v = CUIAnchor.AnchorFromPos(Real, e.MousePosition);
// Drop(v.X, v.Y);
// };
}
public CUIWater() : this(256, 256)
{
}
}
}