using System.Numerics; using System.Text; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.RichText; using Robust.Shared.Collections; using Robust.Shared.Utility; namespace Content.Client._White.UserInterface.Controls; internal struct ChatRichTextEntry { private static readonly Color DefaultColor = new(200, 200, 200); private readonly MarkupTagManager _tagManager; public readonly FormattedMessage Message; /// /// The vertical size of this entry, in pixels. /// public int Height; /// /// The horizontal size of this entry, in pixels. /// public int Width; /// /// The combined text indices in the message's text tags to put line breaks. /// public ValueList LineBreaks; private readonly Dictionary _tagControls = new(); public ChatRichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager) { Message = message; Height = 0; Width = 0; LineBreaks = default; _tagManager = tagManager; var nodeIndex = -1; foreach (var node in Message.Nodes) { nodeIndex++; if (node.Name == null) continue; if (!_tagManager.TryGetMarkupTag(node.Name, null, out var tag) || !tag.TryGetControl(node, out var control)) continue; parent.Children.Add(control); _tagControls.Add(nodeIndex, control); } } /// /// Recalculate line dimensions and where it has line breaks for word wrapping. /// /// The font being used for display. /// The maximum horizontal size of the container of this entry. /// public void Update(Font defaultFont, float maxSizeX, float uiScale) { // This method is gonna suck due to complexity. // Bear with me here. // I am so deeply sorry for the person adding stuff to this in the future. Height = defaultFont.GetHeight(uiScale); LineBreaks.Clear(); int? breakLine; var wordWrap = new WordWrap(maxSizeX); var context = new MarkupDrawingContext(); context.Font.Push(defaultFont); context.Color.Push(DefaultColor); // Go over every node. // Nodes can change the markup drawing context and return additional text. // It's also possible for nodes to return inline controls. They get treated as one large rune. var nodeIndex = -1; foreach (var node in Message.Nodes) { nodeIndex++; var text = ProcessNode(node, context); if (!context.Font.TryPeek(out var font)) font = defaultFont; // And go over every character. foreach (var rune in text.EnumerateRunes()) { if (ProcessRune(ref this, rune, out breakLine)) continue; // Uh just skip unknown characters I guess. if (!font.TryGetCharMetrics(rune, uiScale, out var metrics)) continue; if (ProcessMetric(ref this, metrics, out breakLine)) return; } if (!_tagControls.TryGetValue(nodeIndex, out var control)) continue; if (ProcessRune(ref this, new Rune(' '), out breakLine)) continue; control.Measure(new Vector2(Width, Height)); var desiredSize = control.DesiredPixelSize; var controlMetrics = new CharMetrics( 0, 0, desiredSize.X, desiredSize.X, desiredSize.Y); if (ProcessMetric(ref this, controlMetrics, out breakLine)) return; } Width = wordWrap.FinalizeText(out breakLine); CheckLineBreak(ref this, breakLine); bool ProcessRune(ref ChatRichTextEntry src, Rune rune, out int? outBreakLine) { wordWrap.NextRune(rune, out breakLine, out var breakNewLine, out var skip); CheckLineBreak(ref src, breakLine); CheckLineBreak(ref src, breakNewLine); outBreakLine = breakLine; return skip; } bool ProcessMetric(ref ChatRichTextEntry src, CharMetrics metrics, out int? outBreakLine) { wordWrap.NextMetrics(metrics, out breakLine, out var abort); CheckLineBreak(ref src, breakLine); outBreakLine = breakLine; return abort; } void CheckLineBreak(ref ChatRichTextEntry src, int? line) { if (line is { } l) { src.LineBreaks.Add(l); if (!context.Font.TryPeek(out var font)) font = defaultFont; src.Height += font.GetLineHeight(uiScale); } } } public readonly void Draw( DrawingHandleScreen handle, Font defaultFont, UIBox2 drawBox, float verticalOffset, MarkupDrawingContext context, float uiScale) { context.Clear(); context.Color.Push(DefaultColor); context.Font.Push(defaultFont); //handle.UseShader(MakeNewShader(false, 16)); var globalBreakCounter = 0; var lineBreakIndex = 0; var baseLine = drawBox.TopLeft + new Vector2(0, defaultFont.GetAscent(uiScale) + verticalOffset); var controlYAdvance = 0; var nodeIndex = -1; foreach (var node in Message.Nodes) { nodeIndex++; var text = ProcessNode(node, context); if (!context.Color.TryPeek(out var color) || !context.Font.TryPeek(out var font)) { color = DefaultColor; font = defaultFont; } foreach (var rune in text.EnumerateRunes()) { if (lineBreakIndex < LineBreaks.Count && LineBreaks[lineBreakIndex] == globalBreakCounter) { baseLine = new Vector2(drawBox.Left, baseLine.Y + font.GetLineHeight(uiScale) + controlYAdvance); controlYAdvance = 0; lineBreakIndex += 1; } // Костыльно // Пока поставлю на 0.4. Потом надо адаптировать на 0.5 // MENTION: Это старый вариант обводки с добавлением задней буквы //var baseLineBackground = new Vector2( // baseLine.X - (font.GetLineHeight(uiScale+0.4f)/font.GetLineHeight(uiScale)), // baseLine.Y + (font.GetLineHeight(uiScale+0.4f)/(font.GetLineHeight(uiScale)/2.5f)) //); //font.DrawChar(handle, rune, baseLineBackground, uiScale+0.5f, new Color(0,0,0)); // или 0.4f // MENTION2: Новый варик с шейдером. Шейдер кривой, а код грязный //handle.UseShader(MakeNewShader()); // Ах да, я же шейдеры оставил в ебанинах ахах // Также именно в этом файле пишем приколы со шрифтами. // на уровне отрисовки лол. //var sprite_icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Misc/job_icons.rsi"), "NoId"); //var _iconTexture = IoCManager.Resolve().EntitySysManager.GetEntitySystem().Frame0(sprite_icon); var advance = font.DrawChar(handle, rune, baseLine, uiScale, color, true); baseLine += new Vector2(advance, 0); //handle.DrawTextureRect(_iconTexture, new UIBox2(baseLine += new Vector2(0, 5), new Vector2(6, 6))); //baseLine += new Vector2(10, 0); globalBreakCounter += 1; } if (!_tagControls.TryGetValue(nodeIndex, out var control)) continue; // Почему-то не работает лол //control.Position = new Vector2(baseLine.X, baseLine.Y - defaultFont.GetAscent(uiScale)); control.Measure(new Vector2(Width, Height)); var advanceX = control.DesiredPixelSize.X; controlYAdvance = Math.Max(0, control.DesiredPixelSize.Y - font.GetLineHeight(uiScale)); baseLine += new Vector2(advanceX, 0); } } // Unused shader maker //private ShaderInstance MakeNewShader() //{ // var shaderName = "SelectionOutlineBlack"; // // var instance = IoCManager.Resolve().Index(shaderName).InstanceUnique(); // //instance.SetParameter("outline_width", 1f); // //instance.SetParameter("SCREEN_TEXTURE", viewport.RenderTarget.Texture); // return instance; //} private readonly string ProcessNode(MarkupNode node, MarkupDrawingContext context) { // If a nodes name is null it's a text node. if (node.Name == null) return node.Value.StringValue ?? ""; //Skip the node if there is no markup tag for it. if (!_tagManager.TryGetMarkupTag(node.Name, null, out var tag)) return ""; if (!node.Closing) { tag.PushDrawContext(node, context); return tag.TextBefore(node); } tag.PopDrawContext(node, context); return tag.TextAfter(node); } }