diff --git a/ClassicalSharp/2D/Widgets/Chat/TextGroupWidget.cs b/ClassicalSharp/2D/Widgets/Chat/TextGroupWidget.cs index 5e7b6c0de..11c4d6a38 100644 --- a/ClassicalSharp/2D/Widgets/Chat/TextGroupWidget.cs +++ b/ClassicalSharp/2D/Widgets/Chat/TextGroupWidget.cs @@ -22,7 +22,6 @@ namespace ClassicalSharp.Gui.Widgets { public Texture[] Textures; public bool[] PlaceholderHeight; internal string[] lines; - LinkData[] linkData; int ElementsCount, defaultHeight; readonly Font font, underlineFont; @@ -30,7 +29,6 @@ namespace ClassicalSharp.Gui.Widgets { Textures = new Texture[ElementsCount]; PlaceholderHeight = new bool[ElementsCount]; lines = new string[ElementsCount]; - linkData = new LinkData[ElementsCount]; int height = game.Drawer2D.FontHeight(font, true); game.Drawer2D.ReducePadding(ref height, Utils.Floor(font.Size), 3); @@ -60,10 +58,8 @@ namespace ClassicalSharp.Gui.Widgets { lines[i] = lines[i + 1]; Textures[i].Y1 = y; y += Textures[i].Height; - linkData[i] = linkData[i + 1]; } - linkData[Textures.Length - 1] = default(LinkData); Textures[Textures.Length - 1].ID = 0; // Delete() is called by SetText otherwise. SetText(Textures.Length - 1, text); } @@ -147,46 +143,47 @@ namespace ClassicalSharp.Gui.Widgets { return null; } - string GetUrl(int index, int mouseX) { - Rectangle[] partBounds = linkData[index].bounds; - if (partBounds == null) return null; - Texture tex = Textures[index]; - mouseX -= tex.X1; + unsafe string GetUrl(int index, int mouseX) { + Texture tex = Textures[index]; mouseX -= tex.X1; + DrawTextArgs args = default(DrawTextArgs); + string text = lines[index]; - for (int i = 1; i < partBounds.Length; i += 2) { - if (mouseX >= partBounds[i].Left && mouseX < partBounds[i].Right) - return linkData[index].urls[i]; + char* chars = stackalloc char[lines.Length * 96]; + Portion* portions = stackalloc Portion[(96 / 7) * 2]; + int portionsCount = Reduce(chars, index, portions); + + for (int i = 0, x = 0; i < portionsCount; i++) { + Portion bit = portions[i]; + args.Text = text.Substring(bit.Beg, bit.Len); + args.Font = (bit.ReducedLen & 0x8000) == 0 ? font : underlineFont; + + int width = game.Drawer2D.MeasureSize(ref args).Width; + if (args.Font != font && mouseX >= x && mouseX < x + width) { + return new string(chars, bit.ReducedBeg, bit.ReducedLen); + } + x += width; } return null; } - Texture MakeTexture(int index, string text) { - DrawTextArgs args = new DrawTextArgs(text, font, true); - LinkFlags prevFlags = index > 0 ? linkData[index - 1].flags : 0; - Texture tex; - - if (game.ClassicMode || NextToken(text, 0, ref prevFlags) == -1) { - tex = game.Drawer2D.MakeTextTexture(ref args, 0, 0); - } else { - tex = DrawAdvanced(ref args, index, text); - } - - game.Drawer2D.ReducePadding(ref tex, Utils.Floor(args.Font.Size), 3); - return tex; - } - public void SetText(int index, string text) { game.Graphics.DeleteTexture(ref Textures[index]); - linkData[index] = default(LinkData); Texture tex; - if (!IDrawer2D.EmptyText(text)) { - tex = MakeTexture(index, text); - lines[index] = text; - } else { - tex = default(Texture); - tex.Height = (ushort)(PlaceholderHeight[index] ? defaultHeight : 0); + if (IDrawer2D.EmptyText(text)) { lines[index] = null; + tex = default(Texture); + tex.Height = (ushort)(PlaceholderHeight[index] ? defaultHeight : 0); + } else { + lines[index] = text; + DrawTextArgs args = new DrawTextArgs(text, font, true); + + if (game.ClassicMode || !MightHaveUrls()) { + tex = game.Drawer2D.MakeTextTexture(ref args, 0, 0); + } else { + tex = DrawAdvanced(ref args, index, text); + } + game.Drawer2D.ReducePadding(ref tex, Utils.Floor(args.Font.Size), 3); } tex.X1 = CalcPos(HorizontalAnchor, XOffset, tex.Width, game.Width); @@ -195,15 +192,27 @@ namespace ClassicalSharp.Gui.Widgets { UpdateDimensions(); } + bool MightHaveUrls() { + for (int i = 0; i < lines.Length; i++) { + if (lines[i] == null) continue; + if (lines[i].IndexOf('/') >= 0) return true; + } + return false; + } + unsafe Texture DrawAdvanced(ref DrawTextArgs args, int index, string text) { - LinkData data = Split(index, text); - Size total = Size.Empty; - Size* partSizes = stackalloc Size[data.parts.Length]; - linkData[index] = data; + char* chars = stackalloc char[lines.Length * 96]; + Portion* portions = stackalloc Portion[(96 / 7) * 2]; + int portionsCount = Reduce(chars, index, portions); - for (int i = 0; i < data.parts.Length; i++) { - args.Text = data.parts[i]; - args.Font = (i & 1) == 0 ? font : underlineFont; + Size total = Size.Empty; + Size* partSizes = stackalloc Size[portionsCount]; + + for (int i = 0; i < portionsCount; i++) { + Portion bit = portions[i]; + args.Text = text.Substring(bit.Beg, bit.Len); + args.Font = (bit.ReducedLen & 0x8000) == 0 ? font : underlineFont; + partSizes[i] = game.Drawer2D.MeasureSize(ref args); total.Height = Math.Max(partSizes[i].Height, total.Height); total.Width += partSizes[i].Width; @@ -215,134 +224,134 @@ namespace ClassicalSharp.Gui.Widgets { drawer.SetBitmap(bmp); int x = 0; - for (int i = 0; i < data.parts.Length; i++) { - args.Text = data.parts[i]; - args.Font = (i & 1) == 0 ? font : underlineFont; - Size size = partSizes[i]; + for (int i = 0; i < portionsCount; i++) { + Portion bit = portions[i]; + args.Text = text.Substring(bit.Beg, bit.Len); + args.Font = (bit.ReducedLen & 0x8000) == 0 ? font : underlineFont; drawer.DrawText(ref args, x, 0); - data.bounds[i].X = x; - data.bounds[i].Width = size.Width; - x += size.Width; + x += partSizes[i].Width; } return drawer.Make2DTexture(bmp, total, 0, 0); } } - LinkData Split(int index, string line) { - int start = 0, lastEnd = 0, count = 0; - LinkData data = default(LinkData); - data.parts = new string[GetTokensCount(index, line)]; - data.urls = new string[data.parts.Length]; - data.bounds = new Rectangle[data.parts.Length]; - LinkFlags prevFlags = index > 0 ? linkData[index - 1].flags : 0; - - while ((start = NextToken(line, start, ref prevFlags)) >= 0) { - int nextEnd = line.IndexOf(' ', start); - if (nextEnd == -1) { - nextEnd = line.Length; - data.flags |= LinkFlags.Continue; + unsafe static int NextUrl(char* chars, int i, int len) { + for (; i < len; i++) { + if (chars[i] != 'h') continue; + int left = len - i; + if (left < 7) return -1; // "http://".Length + + // Starts with "http" ? + if (chars[i + 1] != 't' || chars[i + 2] != 't' || chars[i + 3] != 'p') continue; + left -= 4; i += 4; + + // And then with "s://" or "://" ? + if (chars[i] == 's') { left--; i++; } + if (left >= 3 && chars[i] == ':' && chars[i + 1] == '/' && chars[i + 2] == '/') return i; + } + return -1; + } + + unsafe static int ReduceLine(char* chars, ushort* mappings, + int count, int offset, string line) { + bool lineStart = true; + for (int i = 0, last = line.Length - 1; i < line.Length;) { + char cur = line[i]; + + // Trim colour codes and "> " line continues + if (cur == '&' && i < last && IDrawer2D.ValidColCode(line[i + 1])) { + i += 2; continue; + } + if (cur == '>' && i < last && lineStart && line[i + 1] == ' ') { + lineStart = false; i += 2; continue; } - data.AddPart(count, GetPart(line, lastEnd, start)); // word bit - data.AddPart(count + 1, GetPart(line, start, nextEnd)); // url bit - count += 2; - - if ((prevFlags & LinkFlags.Append) != 0) { - string url = linkData[index - 1].LastUrl + data.urls[count - 1]; - data.urls[count - 1] = url; - data.parts[count - 2] = ""; - UpdatePreviousUrls(index - 1, url); - } - - if ((prevFlags & LinkFlags.NewLink) != 0) - data.flags |= LinkFlags.NewLink; - start = nextEnd; - lastEnd = nextEnd; + lineStart = false; + chars[count] = cur; + mappings[count] = (ushort)(offset + i); + i++; count++; } - - if (lastEnd < line.Length) - data.AddPart(count, GetPart(line, lastEnd, line.Length)); // word bit - return data; - } - - void UpdatePreviousUrls(int i, string url) { - while (i >= 0 && linkData[i].urls != null && (linkData[i].flags & LinkFlags.Continue) != 0) { - linkData[i].LastUrl = url; - if (linkData[i].urls.Length > 2 || (linkData[i].flags & LinkFlags.NewLink) != 0) - break; - i--; - } - } - - string GetPart(string line, int start, int end) { - string part = line.Substring(start, end - start); - int lastCol = line.LastIndexOf('&', start, start); - // We may split up a line into say %e - // url and word both need to have %e at the start. - - if (lastCol >= 0 && IDrawer2D.ValidColCode(line, lastCol + 1)) { - part = "&" + line[lastCol + 1] + part; - } - return part; - } - - int NextToken(string line, int start, ref LinkFlags prevFlags) { - bool isWrapped = start == 0 && line.StartsWith("> "); - if ((prevFlags & LinkFlags.Continue) != 0 && isWrapped) { - prevFlags = 0; - if (!Utils.IsUrlPrefix(Utils.StripColours(line), 2)) - prevFlags |= LinkFlags.Append; - else - prevFlags |= LinkFlags.NewLink; - return 2; - } - - prevFlags = LinkFlags.NewLink; - int nextHttp = line.IndexOf("http://", start); - int nextHttps = line.IndexOf("https://", start); - return nextHttp == -1 ? nextHttps : nextHttp; - } - - int GetTokensCount(int index, string line) { - int start = 0, lastEnd = 0, count = 0; - LinkFlags prevFlags = index > 0 ? linkData[index - 1].flags : 0; - - while ((start = NextToken(line, start, ref prevFlags)) >= 0) { - int nextEnd = line.IndexOf(' ', start); - if (nextEnd == -1) - nextEnd = line.Length; - - start = nextEnd; - lastEnd = nextEnd; - count += 2; - } - - if (lastEnd < line.Length) count++; return count; } - struct LinkData { - public Rectangle[] bounds; - public string[] parts, urls; - public LinkFlags flags; - - public void AddPart(int index, string part) { - parts[index] = part; - urls[index] = part; + unsafe static void Output(Portion bit, ushort* mappings, int count, + int target, string[] lines, ref Portion* portions) { + int lineBeg = 0, lineLen = 0, lineEnd = 0, total = 0; + for (int i = 0; i < lines.Length; i++) { + string line = lines[i]; + if (line == null) continue; + + if (i == target) { + lineBeg = total; lineLen = line.Length; + lineEnd = lineBeg + lineEnd; + } + total += line.Length; } - public string LastUrl { - get { return urls[parts.Length - 1]; } - set { urls[parts.Length - 1] = value; } + bit.Beg = mappings[bit.ReducedBeg]; + if (bit.Beg >= lineEnd) return; + + // Map back this reduced portion to original lines + int end = bit.ReducedBeg + (bit.ReducedLen & 0x7FFF); + end = end < count ? mappings[end] : total; + bit.Len = end - bit.Beg; + + // Adjust this reduced portion to lie inside line we care about + if (bit.Beg >= lineBeg) { + } else if (bit.Beg + bit.Len > lineBeg) { + // Clamp start of portion to lie in this line + int underBy = (bit.Beg + bit.Len) - lineBeg; + bit.Beg += underBy; bit.Len -= underBy; + } else { + return; } + + // Clamp length of portion to lie in this line + int overBy = (bit.Beg + bit.Len) - lineEnd; + if (overBy > 0) bit.Len -= overBy; + + bit.Beg -= lineBeg; + if (bit.Len == 0) return; + *portions = bit; portions++; } - [Flags] - enum LinkFlags : byte { - Continue = 2, // "part1" "> part2" type urls - Append = 4, // used for internally combining "part2" and "part2" - NewLink = 8, // used to signify that part2 is a separate url from part1 + struct Portion { public int ReducedBeg, ReducedLen, Beg, Len; } + unsafe int Reduce(char* chars, int target, Portion* portions) { + ushort* mappings = stackalloc ushort[lines.Length * 96]; + Portion* portionsStart = portions; + int count = 0; + + for (int i = 0, offset = 0; i < lines.Length; i++) { + string line = lines[i]; + if (line == null) continue; + + count = ReduceLine(chars, mappings, count, offset, line); + offset += line.Length; + } + + // Now find http:// and https:// urls + int urlEnd = 0; + for (;;) { + int nextUrlStart = NextUrl(chars, urlEnd, count); + if (nextUrlStart == -1) nextUrlStart = count; + + // add normal portion between urls + Portion bit = default(Portion); bit.ReducedBeg = urlEnd; + bit.ReducedLen = nextUrlStart - urlEnd; + Output(bit, mappings, count, target, lines, ref portions); + + if (nextUrlStart == count) break; + // work out how long this url is + urlEnd = nextUrlStart; + for (; urlEnd < count && chars[urlEnd] != ' '; urlEnd++) { } + + // add this url portion + bit = default(Portion); bit.ReducedBeg = nextUrlStart; + bit.ReducedLen = (urlEnd - nextUrlStart) | 0x8000; + Output(bit, mappings, count, target, lines, ref portions); + } + return (int)(portions - portionsStart); } } } \ No newline at end of file