From 235114e60d2de41fc1d01b86b26df0a052cfd48b Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 13 Feb 2023 01:54:01 +0000 Subject: [PATCH 01/12] Fix LineCanvas not respecting X and Y of clip bounds --- Terminal.Gui/Core/Graphs/LineCanvas.cs | 2 +- UnitTests/Core/LineCanvasTests.cs | 52 +++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Core/Graphs/LineCanvas.cs b/Terminal.Gui/Core/Graphs/LineCanvas.cs index ab45c0264..abb17005c 100644 --- a/Terminal.Gui/Core/Graphs/LineCanvas.cs +++ b/Terminal.Gui/Core/Graphs/LineCanvas.cs @@ -65,7 +65,7 @@ namespace Terminal.Gui.Graphs { for (int x = 0; x < inArea.Width; x++) { var intersects = lines - .Select (l => l.Intersects (x, y)) + .Select (l => l.Intersects (inArea.X + x, inArea.Y + y)) .Where (i => i != null) .ToArray (); diff --git a/UnitTests/Core/LineCanvasTests.cs b/UnitTests/Core/LineCanvasTests.cs index 7e988b0de..a56738c72 100644 --- a/UnitTests/Core/LineCanvasTests.cs +++ b/UnitTests/Core/LineCanvasTests.cs @@ -1,4 +1,7 @@ -using Terminal.Gui.Graphs; +using System; +using System.Collections.Generic; +using System.Text; +using Terminal.Gui.Graphs; using Xunit; using Xunit.Abstractions; @@ -129,6 +132,7 @@ namespace Terminal.Gui.CoreTests { TestHelpers.AssertDriverContentsAre (looksLike, output); } + [Fact,AutoInitShutdown] public void TestLineCanvas_Window () { @@ -280,6 +284,52 @@ namespace Terminal.Gui.CoreTests { TestHelpers.AssertDriverContentsAre (looksLike, output); } + + [Theory, AutoInitShutdown] + [InlineData(0,0,@" +═══ +══ +═══")] + [InlineData (1, 0,@" +══ +═ +══")] + [InlineData (2, 0,@" +═ + +═")] + [InlineData (0, 1,@" +══ +═══")] + [InlineData (0, 2,@" +═══")] + public void TestLineCanvasRenderOffset_NoOffset (int xOffset,int yOffset, string expect) + { + var canvas = new LineCanvas (); + canvas.AddLine (new Point (0, 0), 2, Orientation.Horizontal, BorderStyle.Double); + canvas.AddLine (new Point (0, 1), 1, Orientation.Horizontal, BorderStyle.Double); + canvas.AddLine (new Point (0, 2), 2, Orientation.Horizontal, BorderStyle.Double); + + var bmp = canvas.GenerateImage (new Rect (xOffset, yOffset, 3, 3)); + var actual = BmpToString (bmp); + Assert.Equal (expect.TrimStart (), actual); + + } + + private string BmpToString (System.Rune? [,] bmp) + { + var sb = new StringBuilder (); + for (int y = 0; y < bmp.GetLength (1); y++) { + for (int x = 0; x < bmp.GetLength (0); x++) { + sb.Append (bmp [y, x]); + } + sb.AppendLine (); + } + + return sb.ToString ().TrimEnd (); + } + + private View GetCanvas (out LineCanvas canvas) { var v = new View { From fd989430bc2d926b123932f467015fd1aab5f526 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 13 Feb 2023 01:58:19 +0000 Subject: [PATCH 02/12] whitespace formatting fixes --- Terminal.Gui/Core/Graphs/LineCanvas.cs | 137 ++++++++++++------------- UnitTests/Core/LineCanvasTests.cs | 44 ++++---- 2 files changed, 87 insertions(+), 94 deletions(-) diff --git a/Terminal.Gui/Core/Graphs/LineCanvas.cs b/Terminal.Gui/Core/Graphs/LineCanvas.cs index abb17005c..7009c5b71 100644 --- a/Terminal.Gui/Core/Graphs/LineCanvas.cs +++ b/Terminal.Gui/Core/Graphs/LineCanvas.cs @@ -11,10 +11,10 @@ namespace Terminal.Gui.Graphs { /// public class LineCanvas { - + private List lines = new List (); - Dictionary runeResolvers = new Dictionary { + Dictionary runeResolvers = new Dictionary { {IntersectionRuneType.ULCorner,new ULIntersectionRuneResolver()}, {IntersectionRuneType.URCorner,new URIntersectionRuneResolver()}, {IntersectionRuneType.LLCorner,new LLIntersectionRuneResolver()}, @@ -100,15 +100,14 @@ namespace Terminal.Gui.Graphs { } } - private abstract class IntersectionRuneResolver - { + private abstract class IntersectionRuneResolver { readonly Rune round; readonly Rune doubleH; readonly Rune doubleV; readonly Rune doubleBoth; readonly Rune normal; - public IntersectionRuneResolver(Rune round, Rune doubleH, Rune doubleV, Rune doubleBoth, Rune normal) + public IntersectionRuneResolver (Rune round, Rune doubleH, Rune doubleV, Rune doubleBoth, Rune normal) { this.round = round; this.doubleH = doubleH; @@ -121,17 +120,15 @@ namespace Terminal.Gui.Graphs { { var useRounded = intersects.Any (i => i.Line.Style == BorderStyle.Rounded && i.Line.Length != 0); - bool doubleHorizontal = intersects.Any(l=>l.Line.Orientation == Orientation.Horizontal && l.Line.Style == BorderStyle.Double); - bool doubleVertical = intersects.Any(l=>l.Line.Orientation == Orientation.Vertical && l.Line.Style == BorderStyle.Double); + bool doubleHorizontal = intersects.Any (l => l.Line.Orientation == Orientation.Horizontal && l.Line.Style == BorderStyle.Double); + bool doubleVertical = intersects.Any (l => l.Line.Orientation == Orientation.Vertical && l.Line.Style == BorderStyle.Double); - if(doubleHorizontal) - { - return doubleVertical ? doubleBoth : doubleH; + if (doubleHorizontal) { + return doubleVertical ? doubleBoth : doubleH; } - - if(doubleVertical) - { + + if (doubleVertical) { return doubleV; } @@ -139,75 +136,71 @@ namespace Terminal.Gui.Graphs { } } - private class ULIntersectionRuneResolver : IntersectionRuneResolver - { - public ULIntersectionRuneResolver() : - base('╭','╒','╓','╔','┌') + private class ULIntersectionRuneResolver : IntersectionRuneResolver { + public ULIntersectionRuneResolver () : + base ('╭', '╒', '╓', '╔', '┌') { - - } - } - private class URIntersectionRuneResolver : IntersectionRuneResolver - { - public URIntersectionRuneResolver() : - base('╮','╕','╖','╗','┐') - { - } } - private class LLIntersectionRuneResolver : IntersectionRuneResolver - { + private class URIntersectionRuneResolver : IntersectionRuneResolver { - public LLIntersectionRuneResolver() : - base('╰','╘','╙','╚','└') + public URIntersectionRuneResolver () : + base ('╮', '╕', '╖', '╗', '┐') { - + } } - private class LRIntersectionRuneResolver : IntersectionRuneResolver - { - public LRIntersectionRuneResolver() : - base('╯','╛','╜','╝','┘') + private class LLIntersectionRuneResolver : IntersectionRuneResolver { + + public LLIntersectionRuneResolver () : + base ('╰', '╘', '╙', '╚', '└') { - + + } + } + private class LRIntersectionRuneResolver : IntersectionRuneResolver { + public LRIntersectionRuneResolver () : + base ('╯', '╛', '╜', '╝', '┘') + { + } } - private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver - { - public TopTeeIntersectionRuneResolver(): - base('┬','╤','╥','╦','┬'){ - - } + private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver { + public TopTeeIntersectionRuneResolver () : + base ('┬', '╤', '╥', '╦', '┬') + { + + } } - private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver - { - public LeftTeeIntersectionRuneResolver(): - base('├','╞','╟','╠','├'){ - - } + private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver { + public LeftTeeIntersectionRuneResolver () : + base ('├', '╞', '╟', '╠', '├') + { + + } } - private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver - { - public RightTeeIntersectionRuneResolver(): - base('┤','╡','╢','╣','┤'){ - - } + private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver { + public RightTeeIntersectionRuneResolver () : + base ('┤', '╡', '╢', '╣', '┤') + { + + } } - private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver - { - public BottomTeeIntersectionRuneResolver(): - base('┴','╧','╨','╩','┴'){ - - } + private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver { + public BottomTeeIntersectionRuneResolver () : + base ('┴', '╧', '╨', '╩', '┴') + { + + } } - private class CrosshairIntersectionRuneResolver : IntersectionRuneResolver - { - public CrosshairIntersectionRuneResolver(): - base('┼','╪','╫','╬','┼'){ - - } + private class CrosshairIntersectionRuneResolver : IntersectionRuneResolver { + public CrosshairIntersectionRuneResolver () : + base ('┼', '╪', '╫', '╬', '┼') + { + + } } private Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects) @@ -217,7 +210,7 @@ namespace Terminal.Gui.Graphs { var runeType = GetRuneTypeForIntersects (intersects); - if(runeResolvers.ContainsKey (runeType)) { + if (runeResolvers.ContainsKey (runeType)) { return runeResolvers [runeType].GetRuneForIntersects (driver, intersects); } @@ -228,13 +221,13 @@ namespace Terminal.Gui.Graphs { // TODO: maybe make these resolvers to for simplicity? // or for dotted lines later on or that kind of thing? switch (runeType) { - case IntersectionRuneType.None: + case IntersectionRuneType.None: return null; - case IntersectionRuneType.Dot: + case IntersectionRuneType.Dot: return (Rune)'.'; - case IntersectionRuneType.HLine: + case IntersectionRuneType.HLine: return useDouble ? driver.HDLine : driver.HLine; - case IntersectionRuneType.VLine: + case IntersectionRuneType.VLine: return useDouble ? driver.VDLine : driver.VLine; default: throw new Exception ("Could not find resolver or switch case for " + nameof (runeType) + ":" + runeType); } @@ -243,7 +236,7 @@ namespace Terminal.Gui.Graphs { private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition [] intersects) { - if(intersects.All(i=>i.Line.Length == 0)) { + if (intersects.All (i => i.Line.Length == 0)) { return IntersectionRuneType.Dot; } diff --git a/UnitTests/Core/LineCanvasTests.cs b/UnitTests/Core/LineCanvasTests.cs index a56738c72..f6c38c9e3 100644 --- a/UnitTests/Core/LineCanvasTests.cs +++ b/UnitTests/Core/LineCanvasTests.cs @@ -60,7 +60,7 @@ namespace Terminal.Gui.CoreTests { } [InlineData (BorderStyle.Single)] - [InlineData(BorderStyle.Rounded)] + [InlineData (BorderStyle.Rounded)] [Theory, AutoInitShutdown] public void TestLineCanvas_Vertical (BorderStyle style) { @@ -96,7 +96,7 @@ namespace Terminal.Gui.CoreTests { /// Not when they terminate adjacent to one another. /// [Fact, AutoInitShutdown] - public void TestLineCanvas_Corner_NoOverlap() + public void TestLineCanvas_Corner_NoOverlap () { var v = GetCanvas (out var canvas); canvas.AddLine (new Point (0, 0), 1, Orientation.Horizontal, BorderStyle.Single); @@ -130,14 +130,14 @@ namespace Terminal.Gui.CoreTests { │ │"; TestHelpers.AssertDriverContentsAre (looksLike, output); - + } - [Fact,AutoInitShutdown] + [Fact, AutoInitShutdown] public void TestLineCanvas_Window () { var v = GetCanvas (out var canvas); - + // outer box canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, BorderStyle.Single); canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Single); @@ -172,10 +172,10 @@ namespace Terminal.Gui.CoreTests { // outer box canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, BorderStyle.Rounded); - + // BorderStyle.Single is ignored because corner overlaps with the above line which is Rounded // this results in a rounded corner being used. - canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Single); + canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Single); canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, BorderStyle.Rounded); canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Single); @@ -224,8 +224,8 @@ namespace Terminal.Gui.CoreTests { [Theory, AutoInitShutdown] - [InlineData(BorderStyle.Single)] - [InlineData(BorderStyle.Rounded)] + [InlineData (BorderStyle.Single)] + [InlineData (BorderStyle.Rounded)] public void TestLineCanvas_Window_DoubleTop_SingleSides (BorderStyle thinStyle) { var v = GetCanvas (out var canvas); @@ -237,7 +237,7 @@ namespace Terminal.Gui.CoreTests { canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, thinStyle); - canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical,thinStyle); + canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, thinStyle); canvas.AddLine (new Point (0, 2), 9, Orientation.Horizontal, BorderStyle.Double); v.Redraw (v.Bounds); @@ -254,8 +254,8 @@ namespace Terminal.Gui.CoreTests { } [Theory, AutoInitShutdown] - [InlineData(BorderStyle.Single)] - [InlineData(BorderStyle.Rounded)] + [InlineData (BorderStyle.Single)] + [InlineData (BorderStyle.Rounded)] public void TestLineCanvas_Window_SingleTop_DoubleSides (BorderStyle thinStyle) { var v = GetCanvas (out var canvas); @@ -263,8 +263,8 @@ namespace Terminal.Gui.CoreTests { // outer box canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, thinStyle); canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Double); - canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal,thinStyle); - canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Double); + canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, thinStyle); + canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Double); canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, BorderStyle.Double); @@ -286,24 +286,24 @@ namespace Terminal.Gui.CoreTests { [Theory, AutoInitShutdown] - [InlineData(0,0,@" + [InlineData (0, 0, @" ═══ ══ ═══")] - [InlineData (1, 0,@" + [InlineData (1, 0, @" ══ ═ ══")] - [InlineData (2, 0,@" + [InlineData (2, 0, @" ═ ═")] - [InlineData (0, 1,@" + [InlineData (0, 1, @" ══ ═══")] - [InlineData (0, 2,@" + [InlineData (0, 2, @" ═══")] - public void TestLineCanvasRenderOffset_NoOffset (int xOffset,int yOffset, string expect) + public void TestLineCanvasRenderOffset_NoOffset (int xOffset, int yOffset, string expect) { var canvas = new LineCanvas (); canvas.AddLine (new Point (0, 0), 2, Orientation.Horizontal, BorderStyle.Double); @@ -338,8 +338,8 @@ namespace Terminal.Gui.CoreTests { Bounds = new Rect (0, 0, 10, 5) }; - var canvasCopy = canvas = new LineCanvas (); - v.DrawContentComplete += (r)=> canvasCopy.Draw (v, v.Bounds); + var canvasCopy = canvas = new LineCanvas (); + v.DrawContentComplete += (r) => canvasCopy.Draw (v, v.Bounds); return v; } From 319fb8a3d498e46c83e176e8d9ad6f0c616a0a07 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 13 Feb 2023 08:09:37 +0000 Subject: [PATCH 03/12] Add TestLineCanvas_PositiveLocation --- Terminal.Gui/Core/Graphs/LineCanvas.cs | 9 ++++-- UnitTests/Core/LineCanvasTests.cs | 41 ++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Core/Graphs/LineCanvas.cs b/Terminal.Gui/Core/Graphs/LineCanvas.cs index 7009c5b71..ef5de3922 100644 --- a/Terminal.Gui/Core/Graphs/LineCanvas.cs +++ b/Terminal.Gui/Core/Graphs/LineCanvas.cs @@ -89,12 +89,15 @@ namespace Terminal.Gui.Graphs { { var runes = GenerateImage (bounds); - for (int y = bounds.Y; y < bounds.Height; y++) { - for (int x = bounds.X; x < bounds.Width; x++) { + var runeRows = runes.GetLength (0); + var runeCols = runes.GetLength (1); + + for (int y = 0; y < runeRows; y++) { + for (int x = 0; x < runeCols; x++) { var rune = runes [y, x]; if (rune.HasValue) { - view.AddRune (x, y, rune.Value); + view.AddRune (bounds.X + x, bounds.Y + y, rune.Value); } } } diff --git a/UnitTests/Core/LineCanvasTests.cs b/UnitTests/Core/LineCanvasTests.cs index f6c38c9e3..723f202ff 100644 --- a/UnitTests/Core/LineCanvasTests.cs +++ b/UnitTests/Core/LineCanvasTests.cs @@ -284,6 +284,35 @@ namespace Terminal.Gui.CoreTests { TestHelpers.AssertDriverContentsAre (looksLike, output); } + [Fact, AutoInitShutdown] + public void TestLineCanvas_PositiveLocation () + { + var x = 1; + var y = 1; + var v = GetCanvas (out var canvas, x, y); + + // outer box + canvas.AddLine (new Point (x, y), 9, Orientation.Horizontal, BorderStyle.Single); + canvas.AddLine (new Point (x + 9, y + 0), 4, Orientation.Vertical, BorderStyle.Single); + canvas.AddLine (new Point (x + 9, y + 4), -9, Orientation.Horizontal, BorderStyle.Single); + canvas.AddLine (new Point (x + 0, y + 4), -4, Orientation.Vertical, BorderStyle.Single); + + + canvas.AddLine (new Point (x + 5, y + 0), 4, Orientation.Vertical, BorderStyle.Single); + canvas.AddLine (new Point (x + 0, y + 2), 9, Orientation.Horizontal, BorderStyle.Single); + + v.Redraw (v.Bounds); + + string looksLike = +@" + + ┌────┬───┐ + │ │ │ + ├────┼───┤ + │ │ │ + └────┴───┘"; + TestHelpers.AssertDriverContentsAre (looksLike, output); + } [Theory, AutoInitShutdown] [InlineData (0, 0, @" @@ -330,12 +359,20 @@ namespace Terminal.Gui.CoreTests { } - private View GetCanvas (out LineCanvas canvas) + /// + /// Creates a new into which a is rendered + /// at time. + /// + /// The you can draw into. + /// How far to offset the in X + /// How far to offset the in Y + /// + private View GetCanvas (out LineCanvas canvas, int offsetX = 0, int offsetY = 0) { var v = new View { Width = 10, Height = 5, - Bounds = new Rect (0, 0, 10, 5) + Bounds = new Rect (offsetX, offsetY, 10, 5) }; var canvasCopy = canvas = new LineCanvas (); From 90a6c2c34c97c923b46fbfd55a0d3890ee64c235 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 13 Feb 2023 22:08:05 +0000 Subject: [PATCH 04/12] Support for drawing at an offset within client area --- Terminal.Gui/Core/Graphs/LineCanvas.cs | 17 +++++++---- UnitTests/Core/LineCanvasTests.cs | 40 ++++++++++++-------------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/Terminal.Gui/Core/Graphs/LineCanvas.cs b/Terminal.Gui/Core/Graphs/LineCanvas.cs index ef5de3922..dcbf7f2a7 100644 --- a/Terminal.Gui/Core/Graphs/LineCanvas.cs +++ b/Terminal.Gui/Core/Graphs/LineCanvas.cs @@ -79,15 +79,20 @@ namespace Terminal.Gui.Graphs { } /// - /// Draws all the lines that lie within the onto - /// the client area. This method should be called from + /// Draws all the lines that lie within the onto + /// the client area at given . + /// This method should be called from /// . /// /// - /// - public void Draw (View view, Rect bounds) + /// The area of the canvas to draw. + /// The point within the client area of + /// to draw at. + public void Draw (View view, Rect sourceRect, Point? drawOffset = null) { - var runes = GenerateImage (bounds); + var offset = drawOffset ?? Point.Empty; + + var runes = GenerateImage (sourceRect); var runeRows = runes.GetLength (0); var runeCols = runes.GetLength (1); @@ -97,7 +102,7 @@ namespace Terminal.Gui.Graphs { var rune = runes [y, x]; if (rune.HasValue) { - view.AddRune (bounds.X + x, bounds.Y + y, rune.Value); + view.AddRune (offset.X + x, offset.Y + y, rune.Value); } } } diff --git a/UnitTests/Core/LineCanvasTests.cs b/UnitTests/Core/LineCanvasTests.cs index 723f202ff..77055ad54 100644 --- a/UnitTests/Core/LineCanvasTests.cs +++ b/UnitTests/Core/LineCanvasTests.cs @@ -285,32 +285,30 @@ namespace Terminal.Gui.CoreTests { } [Fact, AutoInitShutdown] - public void TestLineCanvas_PositiveLocation () + public void TestLineCanvas_LeaveMargin_Top1_Left1 () { - var x = 1; - var y = 1; - var v = GetCanvas (out var canvas, x, y); + // Draw at 1,1 within client area of View (i.e. leave a top and left margin of 1) + var v = GetCanvas (out var canvas, 1, 1); // outer box - canvas.AddLine (new Point (x, y), 9, Orientation.Horizontal, BorderStyle.Single); - canvas.AddLine (new Point (x + 9, y + 0), 4, Orientation.Vertical, BorderStyle.Single); - canvas.AddLine (new Point (x + 9, y + 4), -9, Orientation.Horizontal, BorderStyle.Single); - canvas.AddLine (new Point (x + 0, y + 4), -4, Orientation.Vertical, BorderStyle.Single); + canvas.AddLine (new Point (0, 0), 8, Orientation.Horizontal, BorderStyle.Single); + canvas.AddLine (new Point (8, 0), 3, Orientation.Vertical, BorderStyle.Single); + canvas.AddLine (new Point (8, 3), -8, Orientation.Horizontal, BorderStyle.Single); + canvas.AddLine (new Point (0, 3), -3, Orientation.Vertical, BorderStyle.Single); - canvas.AddLine (new Point (x + 5, y + 0), 4, Orientation.Vertical, BorderStyle.Single); - canvas.AddLine (new Point (x + 0, y + 2), 9, Orientation.Horizontal, BorderStyle.Single); + canvas.AddLine (new Point (5, 0), 3, Orientation.Vertical, BorderStyle.Single); + canvas.AddLine (new Point (0, 2), 8, Orientation.Horizontal, BorderStyle.Single); v.Redraw (v.Bounds); string looksLike = -@" - - ┌────┬───┐ - │ │ │ - ├────┼───┤ - │ │ │ - └────┴───┘"; +@" + ┌────┬──┐ + │ │ │ + ├────┼──┤ + └────┴──┘ +"; TestHelpers.AssertDriverContentsAre (looksLike, output); } @@ -364,19 +362,19 @@ namespace Terminal.Gui.CoreTests { /// at time. /// /// The you can draw into. - /// How far to offset the in X - /// How far to offset the in Y + /// How far to offset drawing in X + /// How far to offset drawing in Y /// private View GetCanvas (out LineCanvas canvas, int offsetX = 0, int offsetY = 0) { var v = new View { Width = 10, Height = 5, - Bounds = new Rect (offsetX, offsetY, 10, 5) + Bounds = new Rect (0, 0, 10, 5) }; var canvasCopy = canvas = new LineCanvas (); - v.DrawContentComplete += (r) => canvasCopy.Draw (v, v.Bounds); + v.DrawContentComplete += (r) => canvasCopy.Draw (v, v.Bounds, new Point(offsetX,offsetY)); return v; } From 85a44710048901399e1911d48732e33d0594e1a2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 14 Feb 2023 22:11:18 +0000 Subject: [PATCH 05/12] Remove the Draw method from LineCanvas and make GenerateImage easier to use --- Terminal.Gui/Core/Graphs/LineCanvas.cs | 53 +++++++------------------ UICatalog/Scenarios/LineDrawing.cs | 15 ++++++- UnitTests/Core/LineCanvasTests.cs | 54 +++++--------------------- 3 files changed, 36 insertions(+), 86 deletions(-) diff --git a/Terminal.Gui/Core/Graphs/LineCanvas.cs b/Terminal.Gui/Core/Graphs/LineCanvas.cs index dcbf7f2a7..1b1cbe76b 100644 --- a/Terminal.Gui/Core/Graphs/LineCanvas.cs +++ b/Terminal.Gui/Core/Graphs/LineCanvas.cs @@ -48,64 +48,39 @@ namespace Terminal.Gui.Graphs { } /// /// Evaluate all currently defined lines that lie within - /// and generate a 'bitmap' that + /// and map that /// shows what characters (if any) should be rendered at each /// point so that all lines connect up correctly with appropriate /// intersection symbols. /// /// /// - /// Map as 2D array where first index is rows and second is column - public Rune? [,] GenerateImage (Rect inArea) + /// Mapping of all the points within to + /// line or intersection runes which should be drawn there. + public Dictionary GenerateImage (Rect inArea) { - Rune? [,] canvas = new Rune? [inArea.Height, inArea.Width]; + var map = new Dictionary(); // walk through each pixel of the bitmap - for (int y = 0; y < inArea.Height; y++) { - for (int x = 0; x < inArea.Width; x++) { + for (int y = inArea.Y; y < inArea.Height; y++) { + for (int x = inArea.X; x < inArea.Width; x++) { var intersects = lines - .Select (l => l.Intersects (inArea.X + x, inArea.Y + y)) + .Select (l => l.Intersects (x, y)) .Where (i => i != null) .ToArray (); // TODO: use Driver and LineStyle to map - canvas [y, x] = GetRuneForIntersects (Application.Driver, intersects); + var rune = GetRuneForIntersects (Application.Driver, intersects); - } - } - - return canvas; - } - - /// - /// Draws all the lines that lie within the onto - /// the client area at given . - /// This method should be called from - /// . - /// - /// - /// The area of the canvas to draw. - /// The point within the client area of - /// to draw at. - public void Draw (View view, Rect sourceRect, Point? drawOffset = null) - { - var offset = drawOffset ?? Point.Empty; - - var runes = GenerateImage (sourceRect); - - var runeRows = runes.GetLength (0); - var runeCols = runes.GetLength (1); - - for (int y = 0; y < runeRows; y++) { - for (int x = 0; x < runeCols; x++) { - var rune = runes [y, x]; - - if (rune.HasValue) { - view.AddRune (offset.X + x, offset.Y + y, rune.Value); + if(rune != null) + { + map.Add(new Point(x,y),rune.Value); } } } + + return map; } private abstract class IntersectionRuneResolver { diff --git a/UICatalog/Scenarios/LineDrawing.cs b/UICatalog/Scenarios/LineDrawing.cs index f3adac86d..f5670c0c4 100644 --- a/UICatalog/Scenarios/LineDrawing.cs +++ b/UICatalog/Scenarios/LineDrawing.cs @@ -71,7 +71,12 @@ namespace UICatalog.Scenarios { base.Redraw (bounds); Driver.SetAttribute (new Terminal.Gui.Attribute (Color.DarkGray, ColorScheme.Normal.Background)); - grid.Draw (this, bounds); + + + foreach(var p in grid.GenerateImage(bounds)) + { + this.AddRune(p.Key.X,p.Key.Y,p.Value); + } foreach (var swatch in swatches) { Driver.SetAttribute (new Terminal.Gui.Attribute (swatch.Value, ColorScheme.Normal.Background)); @@ -151,7 +156,13 @@ namespace UICatalog.Scenarios { foreach (var kvp in colorLayers) { Driver.SetAttribute (new Terminal.Gui.Attribute (kvp.Key, ColorScheme.Normal.Background)); - canvases [kvp.Value].Draw (this, bounds); + + var canvas = canvases [kvp.Value]; + + foreach(var p in canvas.GenerateImage(bounds)) + { + this.AddRune(p.Key.X,p.Key.Y,p.Value); + } } } public override bool OnMouseEvent (MouseEvent mouseEvent) diff --git a/UnitTests/Core/LineCanvasTests.cs b/UnitTests/Core/LineCanvasTests.cs index 77055ad54..65bc1f3a2 100644 --- a/UnitTests/Core/LineCanvasTests.cs +++ b/UnitTests/Core/LineCanvasTests.cs @@ -312,50 +312,6 @@ namespace Terminal.Gui.CoreTests { TestHelpers.AssertDriverContentsAre (looksLike, output); } - [Theory, AutoInitShutdown] - [InlineData (0, 0, @" -═══ -══ -═══")] - [InlineData (1, 0, @" -══ -═ -══")] - [InlineData (2, 0, @" -═ - -═")] - [InlineData (0, 1, @" -══ -═══")] - [InlineData (0, 2, @" -═══")] - public void TestLineCanvasRenderOffset_NoOffset (int xOffset, int yOffset, string expect) - { - var canvas = new LineCanvas (); - canvas.AddLine (new Point (0, 0), 2, Orientation.Horizontal, BorderStyle.Double); - canvas.AddLine (new Point (0, 1), 1, Orientation.Horizontal, BorderStyle.Double); - canvas.AddLine (new Point (0, 2), 2, Orientation.Horizontal, BorderStyle.Double); - - var bmp = canvas.GenerateImage (new Rect (xOffset, yOffset, 3, 3)); - var actual = BmpToString (bmp); - Assert.Equal (expect.TrimStart (), actual); - - } - - private string BmpToString (System.Rune? [,] bmp) - { - var sb = new StringBuilder (); - for (int y = 0; y < bmp.GetLength (1); y++) { - for (int x = 0; x < bmp.GetLength (0); x++) { - sb.Append (bmp [y, x]); - } - sb.AppendLine (); - } - - return sb.ToString ().TrimEnd (); - } - /// /// Creates a new into which a is rendered @@ -374,7 +330,15 @@ namespace Terminal.Gui.CoreTests { }; var canvasCopy = canvas = new LineCanvas (); - v.DrawContentComplete += (r) => canvasCopy.Draw (v, v.Bounds, new Point(offsetX,offsetY)); + v.DrawContentComplete += (r) => { + foreach(var p in canvasCopy.GenerateImage(v.Bounds)) + { + v.AddRune( + offsetX + p.Key.X, + offsetY + p.Key.Y, + p.Value); + } + }; return v; } From 81a892ad111f139a37cf414ec75eed544d787008 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 4 Dec 2022 13:07:50 +0000 Subject: [PATCH 06/12] Add image scenario --- UICatalog/Scenarios/Images.cs | 123 ++++++++++++++++++++++++++++++++++ UICatalog/UICatalog.csproj | 1 + 2 files changed, 124 insertions(+) create mode 100644 UICatalog/Scenarios/Images.cs diff --git a/UICatalog/Scenarios/Images.cs b/UICatalog/Scenarios/Images.cs new file mode 100644 index 000000000..d0c671b1b --- /dev/null +++ b/UICatalog/Scenarios/Images.cs @@ -0,0 +1,123 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.ColorSpaces; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using Terminal.Gui; +using Attribute = Terminal.Gui.Attribute; + +namespace UICatalog.Scenarios { + [ScenarioMetadata (Name: "Images", Description: "Demonstration of how to render an image with/without true color support.")] + [ScenarioCategory ("Colors")] + public class Images : Scenario + { + public override void Setup () + { + base.Setup (); + + var x = 0; + var y = 0; + + var canTrueColor = Application.Driver.SupportsTrueColorOutput; + + var lblDriverName = new Label ($"Current driver is {Application.Driver.GetType ().Name}") { + X = x, + Y = y++ + }; + Win.Add (lblDriverName); + y++; + + var cbSupportsTrueColor = new CheckBox ("Driver supports true color ") { + X = x, + Y = y++, + Checked = canTrueColor, + CanFocus = false + }; + Win.Add (cbSupportsTrueColor); + + var cbUseTrueColor = new CheckBox ("Use true color") { + X = x, + Y = y++, + Checked = Application.Driver.UseTrueColor, + Enabled = canTrueColor, + }; + cbUseTrueColor.Toggled += (_) => Application.Driver.UseTrueColor = cbUseTrueColor.Checked; + Win.Add (cbUseTrueColor); + + var btnOpenImage = new Button ("Open Image") { + X = x, + Y = y++ + }; + Win.Add (btnOpenImage); + + var imageView = new ImageView () { + X = x, + Y = y++, + Width = Dim.Fill(), + Height = Dim.Fill(), + }; + Win.Add (imageView); + + btnOpenImage.Clicked += () => { + var ofd = new OpenDialog ("Open Image", "Image"); + Application.Run (ofd); + + var path = ofd.FilePath.ToString (); + + if (string.IsNullOrWhiteSpace (path)) { + return; + } + + if(!File.Exists(path)) { + return; + } + + imageView.SetImage(Image.Load (File.ReadAllBytes (path))); + }; + } + + class ImageView : View { + + private Image fullResImage; + private Image matchSize; + + ConcurrentDictionary cache = new ConcurrentDictionary(); + + internal void SetImage (Image image) + { + fullResImage = image; + this.SetNeedsDisplay (); + } + + public override void Redraw (Rect bounds) + { + base.Redraw (bounds); + + if (fullResImage == null) { + return; + } + + // if we have not got a cached resized image of this size + if(matchSize == null || bounds.Width != matchSize.Width || bounds.Height != matchSize.Height) { + + // generate one + matchSize = fullResImage.Clone (x => x.Resize (bounds.Width, bounds.Height)); + } + + for (int y = 0; y < bounds.Height; y++) { + for (int x = 0; x < bounds.Width; x++) { + var rgb = matchSize [x, y]; + + var attr = cache.GetOrAdd (rgb, (rgb) => new Attribute (new TrueColor (), new TrueColor (rgb.R, rgb.G, rgb.B))); + + Driver.SetAttribute(attr); + AddRune (x, y, ' '); + } + } + } + } + } +} \ No newline at end of file diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index 6320c1e99..84f0cdb07 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -19,6 +19,7 @@ TRACE;DEBUG_IDISPOSABLE + From c28d97f203334932c18a88a0de984f7587755907 Mon Sep 17 00:00:00 2001 From: tznind Date: Fri, 17 Feb 2023 21:19:41 +0000 Subject: [PATCH 07/12] Add gif --- .../Scenarios/Spinning_globe_dark_small.gif | Bin 0 -> 22869 bytes .../Scenarios/spinning-globe-attribution.txt | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 UICatalog/Scenarios/Spinning_globe_dark_small.gif create mode 100644 UICatalog/Scenarios/spinning-globe-attribution.txt diff --git a/UICatalog/Scenarios/Spinning_globe_dark_small.gif b/UICatalog/Scenarios/Spinning_globe_dark_small.gif new file mode 100644 index 0000000000000000000000000000000000000000..2618d2cced8f2ee73eaa86bf601cf167da166686 GIT binary patch literal 22869 zcmdSB*7og%h`=BUBB{jCGn9kWsHlX5f=G9FNvDML3@~)}Ffa@-be96s-QC@t z%J^{YeeZq0pWeNn_uG5^3+MS;$6Cj+)+r_PURcDy>yFo*_P2NL@agUC?S+PgD)`*F zeOlU4K0aZ?BjdT4vhU69sYQ{drlyNa%TCc+amm@m4Y}Pz?ZFu?!uoH^D;w(DDowux zNGjP+&rcfof7;mC%*@ZS_H~TTh;8j@YVYjj{&2@V>}zm%va+&rZbeE4vaqjz=(Cfw zxb>YFbnxIN_~)7GwB6>J=9kS5#EY z&dpCu&e+>K_$FIx2lCa{)~>FvL?xyE`TzOD^amg>sjMXaPC*jF!*}o?5W^}c1GG~;MkyE(CbcIhyeCN*{uD{>X-|z0ueVi}8pMOALP;dw=1OfsAd4YiF zh?qzq2m((|Nli=7fI|>a6hA*048=f!P<{ac6quj@k(60eTbGH%mKO>{0WeW*K!HTR zFf2hI0Y9+1eq^*3($icuF%zAL5C{V;4@x*zR8N%;C}TOv%ZFzX1OR6ts`q zJ;x)i5Rz^W)gYjxh&?aP%Ki3;c^*t1s`|#{@iR*n7BZj|9zGyW>xW&oOyslsJn-P- zxvm6ms@zyJX~y0ZYODSVSTuWo#%oss7T+%B!4$EWpjVcPm=PPQB-^jD0@xn_GjZ8ItduY$D~mQQ;eKvKq|%B3ppzEAiie-9t5CQ6+la=s4loAxVZbbtNoe%u?3^5G#MjvuKi2-Glx)NMI$ z|EvaoxtXy5nuiyt45ZJzMDH&6whlYpco?kD`nzYFjy_#=+)QNtz+m$#53(a&z%Ev_ zv%fU!`7(53%I(W;a(0&zph(Mj$MvE&xE=A*kR!qC2~1e>2nevFQI zvN^*%569m#7+)_bW(AUYyJUI&Wm%nNvH8v!;0r{!Y5Nku38#FT(k6$zdF3}K?4Gs6 z#kmM*=FM7IpKZ#h>_6gQ@s{wwIkN=I^jEXQD8JUw&``{mGKu5d56SW8>#kgj6!bV> z`x+fClV$Wid5SgJ@;qqW(*|*|9&Dd3gob~bPs&d67$?dy^2W)iXZmTr=ZH0=hns7` zDU(&R5xPWMNI7}ht?+nKPK(@Fwl++zruQ^P6R9g}i7@~L=3{W!?OctbvT6N7hjiJU zyz=?{7;N3RhhDK&6|r^xV^TWn()MdvtuiooPhlC2Xg#=W7?@;LdDINUR`%F7fKV)r+6nOT2h2L%&g!2IakTmTSU0)WOM zQjoQE^|j$>%nv9KBoJ2?$1Bj990n{Zsc9GifaSx@cmJx(`_>#g#D*cS3FNYM~cND zUv;0NhEwITfnJ)5`N}X}3|mF_shKKvucquHOoe^Y+b|`4ERk|vF#~5+UA4#Y+_tyY zY0va$Pb6}_{_7Ghj*fa$|3_dN{ri;3J;RppKG7g;H%M1|Jg-`RuM7W1qjsY#t~b#X zJ%pNicE-Xl@V%7eO_qzCoSF5wp2eqfZgm&ixiP!5dBO(ASF`ZY0>M}O$tUC8R{Xrb zJgsM^R6IY;g}kmiXkb{dz1~ZEe0ls%1UUg}SieTME{HpGWxPJUKCDk@d+3&<<*<4b zoGOE?WzT%``5DKYTLI_hL+1^V&JTX%`-UokwE;m)9JID>$?EjplLjH;-=qy;7HOO0 zdLYV4)zIa>97AK4_qhx{f&?K(M&9nyD=^sErja4P2Z6CZ9}q`m6h_4Zn}~jdNL&q7 zsGC%Y>-!v%6PIh#WfGvSySA3F9ZtilVG42AO15klV*6|zG-{G+LocuES5TFEj(m#YK>7PL4 zqHmQ149*4v3IOpSNJdq4O?5c1C@&8H5&)Kz{%lErgjLk`_SJ@Wc0pQN5D4Cip%Gqy zzzDEnxGMqFzrGPXjK93Qx4$yna*(ySdER?+b^U7@f4uSn&rh}rku$$dfOHeCNzTq)wsO`cJ1Sd}VGS(Ff zo_STnS#Iit-{yiNq0(>4LNTYrr#S8zWfdAs$6eQP zOq!11)rsk`qb}8JvqBf>NK6}+xvwZqxM#oR%n$u~_Q)-|^O@eN7kz@;(4Go!#6um3 zSITzZd;)`)&&?HXEn3mHTSJ-j+vQ|%CU-dZW|tzV3!XuMDC`AOU>Qx|lD(`=qCqe{ zQbHvJh>%|X!j_(>_wl};N|F+&$SDiX*B)Z%FIbePf`H;Bk_PgxhcHLHZn;>25!2&f zs0ax`v|bE^mT2wc4QH)!oD$`gPP}G+J8Qf)R6{3GCq9Aojq$vjX0oya6&b)A!)m^-nB!3WApGw5F`zZ%o7s8u2%lHhOicv{|hE!0JqKA z|1gpKFDCN9*-!uq5CMc_lvh+%WMT8O!TGs4m;?bJ01(Yp4CyMX>g}rv#{xj0?(Xmr z#MHU|94 z7fLgxKFIA(ef#;kf~vy2BkL>-lok>@ub3?tBl>_=K)W8JTp;^1S!qYA2-ui^cSglp zyo|puNL)*md!_xjxro7%296^Czn1W~0{w506^^Tl(uDYQLmsYxMr*T!c&?@#UqLJbr3$ zXa>?kgOq}a=m`x&6lDYrW%f-7k#Dt{vw;D?^i3vj2r0W>IO%+dk!3GymC@G~vSXz0 z3tN1{h+ibQsZ0xLocW52YzsBY{mpjbY#cj55+a7qmYpR*6S#_sD^{T~O%jOaG>$#T z)aHiS0)-4yztFp{16|%!K=Y~Ifd(%#sW}dipym`h2*uTKNC3is3UIp1|{>>kIRT~ak z#LwGQU#1YQBf$p^r^p#w7Z8`PP2=z5dcPODfk4~u_*OGET)&sdzcing`SFDzBU?&% z5RYmA>E#g;kS>;ZZA1H4-)?7`|EiOUj^U3){ELu(t47`}A^_~|Sd&p!UQw0>fTF=* z{%jN~C%2gZdMh1RtUyU+UwTK;P7V#Mtp~3Rm8?u`?H?TO zZk!Ar?Vev=t)Bi)zM4oR+@!liXngk%Jf#74KAK?+Cj5n|zfY`NAGA-uAF9q1|28BQ zf^6kDVD4~az4=xseLKEr(7$P)tteIc5Xx&~IjV7BT9*o9z?0kLUmHx5lA_dcuP_}B z{E)UDdmU>&nyXwu4o{9_P{PXArtcD~GEXFt55?;^sqVe1##v>hOA-nd%@*kHOgGk# z?&yB9S-{=An0PwXY!K=qLGqY)CEts|N`;H2Y@j>ldG}9i4W`fipI-YAq@6BE59b^) zK|*NF3*F$SRsG%FMR->*P5y0 z#YO|VE+QBE&EL`VJh~!TzH3S;a>Y%8?&qV8tv*{0xWVOfFQI`?La+Wb{JOcH>4YcB ztoQ9Mc?cu-CHE$Y=9$Q4rpkTU%gmRB8s_@WwC7F{;>0f0b6?&xvM);>qs);ZXE_vx z!9BH?$iR7;N?>>$#yQ(xpuK`QjP;%yQkJn%ki{QNgL6l5%Mf!8MpL|}p^M#-u>eFu zpbG7A&E!hsLQ*mBnq~4` z2HL7X>cxUV`8uV7=Q`Lb(_nC3Gf*-3@{JE#IqLbof5@Ggk8JsYuFj}@QXVtG8ut^*fMhaleZY8NFGdF>eth zsj{Q9qOAl8Xhi+=Z|eaHj71cOO@{;9yA~D$M^;wX{MI(NR+e@AFHs zIZIh<`<41->Td#3n#>Okf-yY`yqu&##eMFtU?C+JgVs*;BUH`nJ%KUtnSe;SnJ}{Bd zC4^f#!@tRyl_8+0tK!ouin_K0o5!Xh6hTw&ZV|@3S+N-*U!7T$gGqy^bi9E5?MklC zy9HFO`NlgRzs)NP{iexCyhI^u;CD*wkU#oh99!l8s%`X?$xvBd<~1 z98vg;m_6J`l?LhLN-U_5{6|3jNsf&n?IJ9!$|5`WZvF%Y$r|0Ai^GL!F!3lhLJw=Tb%bmsb`_OJMg2Lzl*XwQKQ2aSx%UEIn3K_~npinPcIjL9> zN?MbNY$ZWSixGnwLhRB=vbApqkliXz;_80yd)UhT@f}<6@4n!DlGEv(FO~eAnv8Y* zzqJmqcuFDr968jzfNG-q?f{n9`?p{Sy8V9z1A+hm{sjgU3j7Bc6_6ZgP64K<2#o#y zJ&*SvU{tpC2mssq0F8|RP(WB4XbOSo>A3|Xcy47iEODaEZ*6C8X}@##Xb1oJ=-?vl z?Am|+`uy?^_Nsv0ma*RV&Xh?jh1JtWIf6e-`0P=7FEv()Ra1qnvg<;rM=XGy&sSfC zx4(5iVJTi^kH%ASfUqf=d`Mt6&}^mdXYP-H`mDd=q}c33UWMRJs~>L-hJCoeI3`y- z?N4FE^efXI>fM4-JFiqnUMN@k8Y?I;bF`>liKCnXIi?8eXm|*$RY_WfZt668F11)y zAA|K{6z+$jJl%>Gf4q1mnA<62J@q~2yNb7_lIeO~^nfAmx8U=hK~#NmRjQCc)5i$$ zEElXtm33>yP* z>wmox4!~I+^e^|gK9twJQCS%it($<0zW#M}u)QLc@8dJNc7BDX^TlftQgOb=7NF)! z)I^Zt@<367K7foZhCJ|zY>j@I4Go7M@vHZRR;QpdiPB>1pd zsgb^}QyL;a1Fl|-x7x4W{ChIrttw!5CvJ)JBG*2kR1Aup4N>r!`* zd&rbp^9lJslAQFBu@@fOAtn4zx@v_-vN?d4evaT`!e`dZ5fWlBmfxmewg1-o`e(!T zFXc%NzZISwFu|?zo_8$Fp~e~}9$J8_HzO8gaV4L?jD%zJb5*~VgwWjy3lHRUO- zBb!{QN-Id*LX~?u3x_|||5dBNO46HZE8Xdrg{~Ib;{rZ|rm)j*Ufd1mb~``JZ5Ldt z-^`gV_@o<)#eCZ%OC<5*Zj^QzW7Gc3s0{;TY|-=)U5)YHAcTZmgmg%Ug_+N z(qKb&n0i^+QlLTxnv>R?DzDu1Qn^eWV*)((F~%96h$vW{cAs2NMq82Av8aJWt~ox* zw@Z&@8MkQwKbomdp#q=r+*8(Eht-jUK4Pm>4takse>%U&h1lv_R!iMh@WW}^OgG}~ zI&gN={IqF_`LJ{$>IpraRav*ZCAz3T-@>Y5I9b*P?mI4H1DoD9S~Yrjv#nhrL##Dc zIl;7ESQ}|rw^vr#V)emfV!Ya@QR#^~R}&voPjTEI9_oKIY=_N+?1g;Jn|IWhpbs`` zf8e#*JKYTzCzHC{OAJu|`uu0Oo#O|SeMMF=o?L1L65ggv2RI*^V&oXqSW@<*hhKkX zp1Zd9)^C10Q0Oo82V>_rD9yfC_WV7ZPrOf73fn^>;mV2Erg&e+zN>f60m7+R4RIR# z1A)k~#7Y|yz0hWVO!$8_GyNOE+aU-{kV^p0K^LIGQ0Og$f4d6E8X$lRnhORN7Zw&3 z7Xfb3>+Y$n=^p?JbRxqM0O-$RV3{9iK5@2pc_m_zdYT~c=PQry02A$SNVP;Y*kRofxw;;u;RM-(~uNabRn8NI2so(3({j*b=icJN_x1k^%w+h4loOdbE$93c& z2@Dll(}q*PbPRhxh5R>hRhl~7M4$GjuW)<@VXY{lByD}9LG{o@hUZXPMsOU zdW`k!t^OK*9d>D}+fESC9`MwiV=XCKL$@kH$MM?S<AX`N0-g^y=w*}cCU_0{%%4xW*-cXyhFM8PU2zlXctZ290g%i;5JNnHUD}B$V&=o2IFf#*{zaIxr) zH5+lvPp_MD=e#(YG}1lq1tvWWHaO4KN%3YS+E9e#cd;WXO4alH z#R)u&5=J=dHj_Emt~lI;ytZbz{Kaw}qN;>t4PvffuIXYzP3gQzZNjCv1d8%JSwvQi7B6 zMgqgYn8L7bFmE1^1XK{~@SxiRul!_Cg36|XvqYkSq8-YxS1|(S;$!#*>>WHEQU3*L z6_nT>{xB}JV;44@(zPi4T>tT7Kuee~9uaGdI;2nQzp*bsKwe({zts@_z z7HYnvxo~Tb4=#y^kVVNE3l=ZF!lS(}gHL0>f`y-vcQac=Q+9iSgu<;<`@Vcrc6!0DtKbT}x#E&N;&_=uodj`$%ddXKEagSl*HBnNuv!cra`(^mT8fNx zEKO*hSZdngOdOxZ78LJkA*?nmUwBTaYFEF$I#>BMkup}`Pn58hTw|e~;Y8=-^)xAO zwJVmlAX{6be5E73%e07Yns;MZsrj4*M(Nz%kfsw{CZ&Sejrrz3Pg$vW(2FK;y@+?e zv>cmkF=~3B`YS`Hpz8=I#7^w)l+>HDpmh2hda*x!j}Gr3?dYP!tjDLD)cKdj&h}qB zR=iC-+2codR|59Ezux=~KFZYrI;;&7dT&QnC+p5WlX&2}&ru^iN0Z`+ zB3{48y}$hw;ldQrZJf1ZJW@P~MxheP0&BhkVr~O2?J}pa>T68rKpim1n9OOunVL2H z4b)vniz!E%QR0;V%`*=k$*Y_&o8u{_&{G`|wXd9MLW-_n;PxznuFOS+TyS| zI42kD%*x{1uPnnEo0x>*#B9B0wzSn7W0^_(0^vtVhWDE;$yK(TS?Il7davowcwMe}JY4vEBW^imjtT1u5>cQH74-Vq+AC+}XG`L*v`_YB-<&xK4uCDao9< zXCI%bQls4Tw%7_}O4*9Jr*-TVIT35LI{C5we@7tvQga(3>tmy+kF8f2Ri5kQz9WSfsg zA}1?x{G|v*J`+ZXK~o3rtlNfVsXy-i`FIP7(jS1cZcuO!pm7$VMd6hEoO3mn8}kCc zD4BHgJ#O~Ji7&p){TRukb%9aOjph5$mCMy6mKrz5wd%H~x{sK4Jb!r?_fg|?Y2Xo) zYn8OyzSc-OfcczIt4N_V=@8Q&=5AwC6sUEuXN6#&TF7Cz&rYg&Ik(o?19I(2Eq4qZ zO?&?()Zy6aK(czIACUIat#bL-H2w9_Gozz5A}gl|Z%>z#lYxw;gDG@w`uNSsC;Rxl zswY;yIxjLE?;QuLef3|hnz14DzQ5#8N{$}#-F>da^o7RzGS213UQH{A)&)q%PJPdu z8ot7cPUm>4OBf%{5+oVzB1})1jo=hC*Vla0Pqkt$-i}j2nesufO0j%WF~EjjOe+EqulEjbkh8 z^S6^-F--3e_W`oL*S152Mt=45|PqXhPv_TiXs(tOrCsopA)M|oy67q$Bly_ zXFT#AXSA3mykd5h=F+RslIFMEsfW#^{9l!Rl8+U9c}x;n6J?-09V{lp77j_XV^4=a3UTuZ zc6i-AEQqjv9LfpoJd_^C7gmf6XkQx-;hb?}_UqJptS>8MHba|6SeSFOlaQ?67)UaZ!SX3Rs;g(Vmxcn9I)$M%PfY7 zuTohx%5BD%_t`pWhTiGfi2v?zwp|Q44q?6ZL5bKZ-u%a27!Co3^8x_;(fqu;;kT6k zbNwkb2_FJU1Onpt!32f30$dV~%)Axg3M8toz5&P;%>@7#mlWJ)(zhCXo3J$u4h;$< zpakLpr9J(#bN<7(Yi~pIYke!5TTAP^X}7)b;ND5l(fP&c)y1#j#JcR6WA_!QNUAJt z6n;Fo^HA?~F!6vus&M{bQAjVFWxypjW4i*KIKkvfJ)R-;noPM6mV~}y!EjS+1wspEyFCn7Seb+qhsTf8A?b6oa zJR@0Zv?P#LS!u3WiL=7c8edcL$k#rd8*jXRa4?$YImUStDr8rer=da{Bujl{*c=`w zD*AK^Inm|#P>?{4($TPPnd?4*I!(!oE~B;h{a$yQ?WTgEEXq3QrtnOePB5DeovYQt zce(2RnfgPE><+V-6`T5F8@c8<-e6KnC#S`ql5d1)ub|^qovuEAeh>Y+++9Mi%op}B zxX&E~`nzg-na*6E?7&OaUDIC~ zlj#?5^I46_kd$YcHrQ;bPkZN`6g+3 z?vvT5Y3KVa7Q#>utytUTuBi;CZf16#6p%}f_mgmAj$D%`=>9Z%B2AtAQc5cJ?8_k{ zpmd34_ZYWjkoi_diU{RsnHaG!x(J*Fye{sS8&~HZH1ONv9g<`K@eK10;X_8I*{WWQ=E>f?7`uUl32178J)9@>ji}Ej z0Z}&RQ|4&w`Ly*&NKDY1>a?gJfH{X4S9mAVkI*gxX)u64s7cI{kf$JytWO2vjew|I z;VT`RC4QiY9lpC<5f4w`e65@MFv3VF+aDw?#@P=>!!`%Iw+pNHuXjqyi~J$_1&kke zYc_D##LgLggll#853W}$iumG#dhlr%^}LRJ_63go>FM+(7cMB?9ZVkJK0b{n$37R39v?B_!x zOR74%GHU!v8wLg&pQ!3TL=Do$H#}uCl?FZ zwP#b8xY@uTM+Ila`W3zt1U&qmkyI@K#A5=fn|4}1p1OyP_d?y=O6RcT39 zC0+l`&pBOj?A5iS9Q^B81f5xb=)`lwwvK=uGksLZCk$5RXOVzVH%5c$n;ba(aALsHU3Si?lDO&x+j3!1-w`0`Ej*OZ{P`)ac zHU3@J_=>x#sKvaCF?X9ul7E^;oQ~?!rLK*7w@Zo+4OCLW#|h&0)b-2sMMn758OB(i z^tBs0bLz+<&pHZ*`V=W`NtBiyQMXPXrr!?J;eBWdVncJa+}F)>sbb&8e8_LL{1S|; zv&<(hidFp-W~wn)I8H{u5DjThW+`svmNif-DU$`2W*{&cCEoenyRN$L=vIn10z_=0 zd=zECx)nAbOsntgZ|_#cEj!y*z{ zF6+l#@$G}^L|eFJb3?vrPRL(455VdhLLoZ6HZp!?&C)Ef*$oMP@&?<_ERI@7T~wZh zkPnn8TgN@mBT|oV4WeIDJt8}W+OWpc*{x>Jy87+j&e4}xede-{8wuhQUw2y+0C**x zVh6CHF%45MRE4gO7jo3#?rWwg6Es#(m8Mu{AlA0{%zoAeDR&X-x^aV;^GKMySn$2i zbGaBml`hBz3W>awwWKW~4MMSa%PxbJn|zsbxqO$H;S1519Oxw=d>KfQ?YvUJaP{<`ipQ zBy?1*RD5$#5jyz0o;jkVsqwVU?ZAy6v6>@dx_#?&&|DGZDpB!zt z-|Yw=0f>s={dGH|B`RA_ zJ?Y-XE60b4EwH|dFq6~>-R_+*W4Vsg=AEPYOp& zKD<&-%XRPUwuQ)do;>{D7Q*vmBx|<9UHuuuxad>P>^wu62hhTj7I0i)nJa5>Qnb_O zgcnFLKF1Sq3kZ9gTI@`#g5MUVizp;48II z$r<;mrTc=g@ReB2`8582-e`q@!XdZU6*0VkTiy}>WA3GeB|!o}AQT!Tzz;}_%E|h> znSh5O!s_blKrz6&s+QKuhEl(r_K5EG+Mff1p}qLGg`s|Es&!&^ZhU$%qX9j?w!X9( zblXfcY;Elw?Hvr29$)O9wuE(F;?}NB?`5ACXIA^BlYml#?H(5eK6)q^OIZr6h6Qq2 z?grTpwnr@R`8^fEE{XV!DMZI!+RibV+^FiJf*+*FdUFubgmUt;3b@_fd)^~JYC4eA zQFi;yzDV!L7PTLz+x$Gzh*p?eij}q2RB(S)-oiqNHEu64ERJ5Q`(oXW!dV<;?QM@Q z_0_P)uyU`*BcUs+PxjbdXIqbZ)i?YtL3yn{g(oIZjLy zBBZ|Pq{TN|3zsx#cs4lq%k)c;WBt)+Z%iopoAlo!hfnMu>Un)=f?giUd?6U%T%9?q zc&$Y1eK&q$!Il>m^ucGXR?3&Z`lCy{@i4bxrZdO0@$`>)$F7WqM5=^3z7)v`A#D|^^b?#w4 zqT8^iLGEI;W^&D&ak&{J_dMq13IpABMd3gq6M0fo5sLJvrZF1H!DpRv30kI>jA&at z_0+sQcUe>pdlq~(U#vMPuYfCg44oM{Y`L9Z?=72AgtRkTEBpcHPt&}3(zBISSOv{5 zGbNs}DtaIm3ND`@7Q$499Yf9R5!;$u1*1V9z`2!*o(Yw>uI*jz!K$5o#|4lU+Gcv0 zn5(gDRcpKUH;9Ta`_2YiNi%+S_i)LD-{y7;$=&YN*1N|HVBYFS1FrTBPo$paHxee8 z^K{aU2-ub~rM!Z4v4@7)H0!x*0jtjpB~KD&y1tf6^B!0$4v5~zclz6BKmC`5uy^Y8 z>Ce-%+rnUTHl}W76D^gLiWGpX7H+Dsd1D;lgwRM}3o*fFW-BXD-9h$&afDc`-otB% zQ#$DOnt+(UqVWfbbvd?`*qSc$av9Ftf4SgY-*`z3kZx*Oie;u3;u95LzF5f&=DFTP zRrl2nfw&^wwhF67ukcbp4~UkECP<-+=r<2!)~c$D{K1}d4~V9NFTCBSYkktlK+z3+ zttW#6hX77E!GEp+pm1O~;@0B_0V5#)>o0^QBn8ApM%`|b6k>~tGs8eRiIr73CAR|w zeszssQ*%qh&o)>~Wn*Vgb$dUyAh362v~OSnKIGq2J~rPoxzw?+y1Kl9T-%=C7?|EU z9NTM{?(RCeY(6Qjnz{P3c;1F59hvq+{@!ESS9vm7;-(}WuTzD&kW6suGYyu2A>DQl zvWY8kg57L^Z=m0h*rY6sS`@Sf<8CMh zkR%C(v!#nD|C=Y==Vz(|{+t83So;{grf#PJZ@TUCL3p!Q#@WXh=PwqWA3%EjzwBB! z2TNVoFB1igQ@Nfw>M6g5xGeRRzpn0|-P1d)+TaQQp15|tv&BoFkTL6RaHIY1uAbWP z(oY80px41_-Yae_eOykgo$1#11=Yu-$z-L~1JCyY7hT99os)s&m;@$MI!f0$n6D21 zk{cBaId3w`l=BoeJV&kXNn~@X_aQZf>ucF>!hpO-3g!B9nRs1z00m$x09u&oPa4mt17l6(dOg{t z2Un;VB2c#Zfd?++yE_P_E)g43QcJ5GyS=42CwcFqbA~4w+UQG2*TX0<#)rS+ZV%#j@iNCm|n)~ zP5#^Eus-$w2IgCeZ{q-gh`8{7xdZU9a0D_Nl<1!o0m6r1Gm1+}OEAEK43GdCn_E=Y z^t}{vyN;h5itTL7>+0?OF)$cZlb_i)J~25o-IUb{n_OJ#pIJ>o7tRMR*YE7C9c-`f zotzw=AD><=UHsnMz;)f(9+@cCek<08Px@4yZ(8^rfO+5Q5}frj^f70DOJ`Jid-yYP z)8CykC>cAJmucD-+$J*dd~vm}D9M&Z@W5o#~r z$xGJZ*Tr%UXRAHHjU+u{QYru>U<0aEtm}%lO4jd!#}WEL?e7Iv^mTg}$4^1^&%2MGJ-P%|Rv}2)=R-@DdP@ACf7$>1cx*t0GR$I+f zv2TCh6V|u#dDb!MJHVNz>}BG9&%l*jesWPr&?Lbi@ZNIuM-?J(H-j%M`>IhepsZRS zoWXKrIpltjI-R{&_5A1;T?HFhsf(O!&{DDi{Ld!pW0 zC5xH|il-)1(=fj4*w}bdn8>q{=Er&>+@~xOO)9w`12XEP> zOyXu3XI^2IOn!bbqXz@_yrC}Nwt8D5-@5cFuK-8^5NyNO8v?r zc~uzhGfVI4VnkvR3URhH%}iNXWtJ+G&tLg4=0S`4hLm5L9CCH19qjMqo@o znEQOvIN^a{|2YoRf~T6YcRpi~(0kT#8?Xz`y7@P@kN{EKC46!l;nk#UzvfPup zz6fg4=gg6}D!dvy@CXJ%i2p~M|MtrH?=xq>Z7U88PxzM%Cqp6tsMrLMKzx!vBq=gNhhp2uM*PSvnH{Uxj*Olh1Klv%QCLC3!5@d^TcA`<#P!dS0R16d zVDZ~BDCciw`7aX;^^(67z?O}6F*&(X}iAhlD;N{+^A^UT? z$HH@7>kG(B|B>eKs{TQpbKe=}vN-Pjmu}yKkEe=Eb?aM(c`}r&81#RSdQ_&1Mw7at{*aT~jv$7nKp5HJS2CwL$X2hf1T#V@*q$(d8W~hK3QXOLTjqnl?=X5VyiUF9|9~6^GyW)bPO%IV za!yEd6@!XQz8m>WwD!4HMr}=}Vk?HmvUIMBH34gIkz;4UBgB@B@mgC?u^wjKNPS6r zpC;8E8bFiiI=-bO?MskvW*6AxW-eb&QvFqV+DE*gXJzpr`55WHCNn`p4`!=forwkwez1Hc_ z^*G~q)c?qE2Uqz|#+=#5;DoTJ1H>Jjj@`^NzN3T&0n4wPz~~pi7Ca`5Zx8X?P{uOC z4r+Y?2my8WT?qed3qPdBx)F~P;B@n1Ku=4ja$1e~Hpc~qmO0P-Q-)`_Bl-X_uu;{hRyx*y#{fZvVFe+ezt0ycK>&0uKTsekGqI_%rL`rD zH@6lHY=%Sn2b%hS^#2?i4;}_|f};>=gNsXp6RWLL;911b3T%0QdF^PSNg#)}G;lZc z;P=w;AGq?Sy6>0A{CGFitoZj287kjBwBSz7D>kS!YvU6OkQz#pbR@GbIkqiM#d**F6F9QPg`g?{6?>; z=lZ&2Co@TL!j`hGx@S(@3sB5Oak{va^^F;wrgY_ic1nBW>hasPK4QfA{(PDa?{@UW zo{OtiTFKeZ<&JtAQg;`PvmGinUC=e{*W=@j&~w@Y@2|(aN7d>cxGelk?>Mp!tc=)L$fuG7Golhu7pVeJ_Wv&Kc`xslHhI@AvC|z^_E$fjlb677 zUC#K{TVqJ}>}3rP@$3TXYIzLU1|<*&#wxdR1|KxAOJXJCA4=xjpUsi{01YoU9KN~9 z?&Dc?>7UJd+Xh_me30}p0TDUK2Foez0(Hm}wM@#Ten~yC&>V$=%fuhEDJuce2B%#0 zTOXe{+!R+4UgLfqN5zq^3@~{X=^qOBPrCWXDa7kRr(=XF8Rh)V7rviv&T<4SrWs#) zGzDMf?_^Ik-;terucTP>ASB%Kr;@p&;^>1Cw9fumX_>nYe;Q+IZ#Uu!+2%X`TNeY6 z3`i48WazK|cJsNQ-;NR%3r<0UFo{seZ&bYYRTZ)jhXZF|>e@R#WS3(SV9i*-yIcrm zXsGkk2yzHi1p<`dB?%4L!*j!^Xx^vCJZ083}B_gvkXTvH4l zMJ*p6%*?hz&o{8QJn|q07u>Ky-I{Y;`M57g)RsBk)1{c3^@S!K13#BG>=sA!wFJzq z-s&QW9-MmYjrLRcll(Q_N-=i(X;JfSd~9&j^3BqqJ1J-fac=EC1$X7bb3~^Fg{{^&9MNFWy{a2NP&Y< zdBjo|f@De~xVS=%2Z68B5!7?5=O}wF#798_j`aKZeb-*a&+#f+sA0$#}uVr=~ z%>p8RF`{#F$I&ACyF%owuC7og6VlPW@e<$#O;!@MByvpMLWeciWhz0swrYpZXvl>> zz5f5z?f=a2-)B%#Qcyt9|3Y?g2@Lk;w+w;@!^F@S5cD6&t^rvDYJw6N$5DX9#_pcp z*Jua{6A6N~;b8!BHfg+X@=H_Ec+DS>*0vEbAgyp_VrrvyZLJB2V+I3Iw4K=P_05y` zpQNav!fn{+?VR@TQw~d8pEpU8Mk|wiu4wU_ogUX&B_2vk*s4C`k_ZwodSU*Qq0y$x z<132?!msJWyxly>c8baJzDA3fQ5;t5NjMuct#+*%OV4obW|bG**=0G@Sz7Z~c(<$a2A zWFo+tZ|M+%|J=lA=}G3hyAvO9LvA4<1+z!@dYT@j3#58@r_n@6&%aV|vj6O8i>gyKx@#p*ka>&Mey)Qx)FAHmPK z7T^(RlpL_Xz*mV@wD3G6l#aGduE(SmdTK%fiz8oZXjq1ntD4#gU2ra46fy|<0z_+s zF5h{vl`Kn50aj$>X1Poq3K%p{?ytH!jybL7dapF-=Y@c{9Zf_1Sot!-IO5jw4n%N{ zS&1q3JURclDvNNtw!+C-f+6{>BW#g~wR~O&_3O|aKeJ^#IvSs4FRI8860J$jZ?Sb= z>jYfW#?;-)Gsj1TAsKL?t*W^j)7%Ni$|RSnzGkP}RZ$X}&OXCNF57hpaKu(UH4Nn7 zJQuC$SdX*75E>W#I7Na`pls8o3U|=1%a8FvLPK>R3-5a@<9{9_a~Osgj&op4#r!MmN3FOA)QF2CG7^K%STocMF~?V#kv$zAeS|26#JHS&y#X4{$NQgczt zO0n@G%K|QZC~l>8Vn~M{*m6cgpnLdk3;*rG{{pi4jl4?&l9Peyp#O9B6A}{=emj;7 z`2?7WgyjF8>wjbd0TmUmK-f|O093^&%zNKKiZUS(XxBSnd&kh*;l7DJ^3>oI`PIM( zOziy_ZF03{W~Otaiwwh;1IK3=7kG}Aw@=3F+rYppPBjyv`5%-|t_VI9Sd6+9QG2v6 zht}rnLcT^@!(X%WtJ(r?M3*0FE%cdj%#=Lwe-kl~ny*K4)fmaq3=@~5P@S$Zs8`t= z-*N_asDEWCtKLq>csPxCXmXjk!hWn%LeT-B+yIKxN4%0%sC?G7t-)f;%UOJDd%}~c zbeG`fp1^d`w#H|lg39HHWu|3J+F3Q`-gxoGK(T0p0U6KoIs{asr;%%Ux<7j(Hy6$h znImfz(6&wwYwY|SFAgN~jQOaI89wFLoS3S!!-e`{>qM@Lnd{1J?yW2Z?{8CyuT}U= zPGvlo2GadqElu;k$Geq)u{!~BBg<3P-ZFXB%^P0_`}}9&igM-q5o15 zD>Kace_p2dSt6_MtyqMWuNzZd^7ZyF1cG=C7j2h3qmv?qNsv1h5MVw3i!vvA&pl*q zdc8n`HjHOBrWVRcP2KqE!;@s163vz9H|Buxc;5DU!Ts;BSQ;1~B`t_?(LrY=QG1;3 zTl?raI=V_i@HjsQ%P)>WduK^PZ=O#1Jt!2WAwCvLOK+@MAR!>(D79v%^%At6!8$f) zWi1mM7!6y;duKR=%AIBsGPxfYPe5&EV7zY;q5)ZjcD~ ztLvKMoHHq~6W83H;~*FB)(kv<-PkdHJmA>|d@TP9XY9qsdFMfn`nb3g#ItC#_uCm= zJSUZDEni&TWZH~_#Y%pxlk&*neL-5!BSE^r?bHhL$S+OF9?@BO_aWGBg2gc9HuI0(j*ByC zj9zSeU2<@;^ZQ3?%pOhV_5Gr*u|`G*&II7Dg8yAmON=lnU?KkPCpGvr=~6O6;hgB6 z9V0wzQ|C_{2Ry2c5KbYx#a#-P;m{g3nsz&yj}Z!E0~bz}cGBXWipq*Rf^*qtQXTV1 z)b9^Ou28eUmjYJv9E}FXd^OwReD9-_fN}yTlaXWP{|=ph!(eJt8HWWKIr+c; z_ACZL2Pgpoa4_bw@^Tmu^UrpUgy0ya3rq|IsK&HcRKTFs^#i1O2!+x-Gz5ZzUZDux z@2Uo=GufZ!=I0_CyP3iGe%dCDv7IkLz~$h~SD&INTPL%-^rOQno&e8&Q{~W84%+eS zt(gKVn32JWl+L~P;nBBl=cp&2QHmerCYY}cWwV9eQ6Azj{WLq^$D&Wz>ei!bBv7+v z1Y}3`Df#nW&ReA{_`f3`zviI$|5)ou(cFdFYk1kI$Zi{j27E=K7v=n1-aeBZQJhToo-N>4*cEgQ zE}@QOYv{I8%=W&H_1N1xiyCfvFMN{I8xG(L_E>ajPks*#k*!!y3k<(O4}9p||E%@9 z)^?uvar;rq<;u&7%d@jzReP<%;f5F9`et5aVN*e$ud!4K5q@Cta`k0*-m7cPHK`t) zkg?C!>mrB(SY@mOI>yEttrY{@Xc%@CV{YX2H%&)p2F3r9DGZcBW?1Ng@-pt2UJXNE zaXyiyL%D+B~p^Z-gt>DjXM~#=$|F2 zFuk;#YzW+R6=^jxAeipSR=S$)xzC8ngI9bV^*Z|*-O^|}fXed96gm5p3Eym+W~FLF zJAYjmtC%}e#(Qw&Ohv3m8%2-gQFTHOei+4c?v~|;1;4necTu7t?^-XZz?{453U#z3 z2w2Ng;ci~A9lD1P$LiJ3QKY`7DugTddKTHztOK7BC>TrlL$>YB@lX08Pq5 zCcub9FtE3I_~S67sBUz$5?X{Ag(lYbwUMeTW>*#vZ^zTkrMRl8ebfas7xOUYGLyV81(_Ce9 zA9T;&w2N(=H**0gONTWaY*n|Wo^w=nI(qIv$>uUXbv=|=^Ueo%$y?M^Z={rexD~r0?>Ue1s7X{XI^0=XdnD8TXc(qL8)@(k zy3Kpy^|e>kU(_S7(QRT3N)$4#$q8~C2(wzj@Qdz|+vhYU9>-?hBwh3M4nyG-_BLTyw^b35((?^Zz{uk=+WAx4j%J4il z>BTKum^d(FQ%bk}9o!rsMkYbUtkHr_dJZx|xx#w*n|Ux&tx$gG2E%1}8EtyKfQ^?? zN9%w{f<+RrX4Ya~_%UmVh#82ZB#Xq1z~wElI!oQc$~x=j=LMzX3DFy7Wlk8jvNA79 z+*D@$l#F?0^Kp;MyE;0&Ouw_&*{-_baqJe7Bnl``8dBUeuPuA5xn0F=`99u!3T}>Q zc*uLcvLP1reyw38FW$XbYrnrTcC#&5IC-x=+p%?;7BAK!wsX_Hh_0CB)RE1kCI05Z zq4V?3cv!c1dxAVeM4O5hZMvwGOo*$#cgF47Wu>-lJ+?`(9MHG Date: Fri, 17 Feb 2023 22:56:02 +0000 Subject: [PATCH 08/12] spinning world --- UICatalog/Scenarios/Animation.cs | 192 +++++++++++++++++++++++++++++++ UICatalog/Scenarios/Images.cs | 123 -------------------- UICatalog/UICatalog.csproj | 3 + 3 files changed, 195 insertions(+), 123 deletions(-) create mode 100644 UICatalog/Scenarios/Animation.cs delete mode 100644 UICatalog/Scenarios/Images.cs diff --git a/UICatalog/Scenarios/Animation.cs b/UICatalog/Scenarios/Animation.cs new file mode 100644 index 000000000..231b0f399 --- /dev/null +++ b/UICatalog/Scenarios/Animation.cs @@ -0,0 +1,192 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.ColorSpaces; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Terminal.Gui; +using Attribute = Terminal.Gui.Attribute; + +namespace UICatalog.Scenarios { + [ScenarioMetadata (Name: "Animation", Description: "Demonstration of how to render animated images with threading.")] + [ScenarioCategory ("Colors")] + public class Animation : Scenario + { + private bool isDisposed; + + public override void Setup () + { + base.Setup (); + + var x = 0; + var y = 0; + + var imageView = new ImageView () { + X = x, + Y = y++, + Width = Dim.Fill(), + Height = Dim.Fill(), + }; + Win.Add (imageView); + + var dir = new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); + + var f = new FileInfo( + Path.Combine(dir.FullName,"Scenarios","Spinning_globe_dark_small.gif")); + if(!f.Exists) + { + MessageBox.ErrorQuery("Could not find gif","Could not find "+ f.FullName,"Ok"); + return; + } + + imageView.SetImage(Image.Load (File.ReadAllBytes (f.FullName))); + + Task.Run(()=>{ + while(!isDisposed) + { + Application.MainLoop.Invoke(()=> + { + imageView.NextFrame(); + imageView.SetNeedsDisplay(); + }); + + Task.Delay(100).Wait(); + } + }); + } + + protected override void Dispose(bool disposing) + { + isDisposed = true; + base.Dispose(); + } + + // This is a C# port of https://github.com/andraaspar/bitmap-to-braille by Andraaspar + + /// + /// Renders an image as unicode Braille. + /// + public class BitmapToBraille + { + + public const int CHAR_WIDTH = 2; + public const int CHAR_HEIGHT = 4; + + const string CHARS = " ⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇⠈⠉⠊⠋⠌⠍⠎⠏⡈⡉⡊⡋⡌⡍⡎⡏⠐⠑⠒⠓⠔⠕⠖⠗⡐⡑⡒⡓⡔⡕⡖⡗⠘⠙⠚⠛⠜⠝⠞⠟⡘⡙⡚⡛⡜⡝⡞⡟⠠⠡⠢⠣⠤⠥⠦⠧⡠⡡⡢⡣⡤⡥⡦⡧⠨⠩⠪⠫⠬⠭⠮⠯⡨⡩⡪⡫⡬⡭⡮⡯⠰⠱⠲⠳⠴⠵⠶⠷⡰⡱⡲⡳⡴⡵⡶⡷⠸⠹⠺⠻⠼⠽⠾⠿⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⣀⣁⣂⣃⣄⣅⣆⣇⢈⢉⢊⢋⢌⢍⢎⢏⣈⣉⣊⣋⣌⣍⣎⣏⢐⢑⢒⢓⢔⢕⢖⢗⣐⣑⣒⣓⣔⣕⣖⣗⢘⢙⢚⢛⢜⢝⢞⢟⣘⣙⣚⣛⣜⣝⣞⣟⢠⢡⢢⢣⢤⢥⢦⢧⣠⣡⣢⣣⣤⣥⣦⣧⢨⢩⢪⢫⢬⢭⢮⢯⣨⣩⣪⣫⣬⣭⣮⣯⢰⢱⢲⢳⢴⢵⢶⢷⣰⣱⣲⣳⣴⣵⣶⣷⢸⢹⢺⢻⢼⢽⢾⢿⣸⣹⣺⣻⣼⣽⣾⣿"; + + public int WidthPixels {get; } + public int HeightPixels { get; } + + public Func PixelIsLit {get;} + + public BitmapToBraille (int widthPixels, int heightPixels, Func pixelIsLit) + { + WidthPixels = widthPixels; + HeightPixels = heightPixels; + PixelIsLit = pixelIsLit; + } + + public string GenerateImage() { + int imageHeightChars = (int) Math.Ceiling((double)HeightPixels / CHAR_HEIGHT); + int imageWidthChars = (int) Math.Ceiling((double)WidthPixels / CHAR_WIDTH); + + var result = new StringBuilder(); + + for (int y = 0; y < imageHeightChars; y++) { + + for (int x = 0; x < imageWidthChars; x++) { + int baseX = x * CHAR_WIDTH; + int baseY = y * CHAR_HEIGHT; + + int charIndex = 0; + int value = 1; + + for (int charX = 0; charX < CHAR_WIDTH; charX++) { + for (int charY = 0; charY < CHAR_HEIGHT; charY++) { + int bitmapX = baseX + charX; + int bitmapY = baseY + charY; + bool pixelExists = bitmapX < WidthPixels && bitmapY < HeightPixels; + + if (pixelExists && PixelIsLit(bitmapX, bitmapY)) { + charIndex += value; + } + value *= 2; + } + } + + result.Append(CHARS[charIndex]); + } + result.Append('\n'); + } + return result.ToString().TrimEnd(); + } + } + + class ImageView : View { + private int frameCount; + private int currentFrame = 0; + + private Image[] fullResImages; + private Image[] matchSizes; + + internal void SetImage (Image image) + { + frameCount = image.Frames.Count; + + fullResImages = new Image[frameCount]; + + for(int i=0;iIsLit(img,x,y)); + + var pix = braille.GenerateImage(); + return pix.Split('\n'); + } + + private bool IsLit (Image img, int x, int y) + { + var rgb = img[x,y]; + return rgb.R + rgb.G + rgb.B > 50; + } + } + } +} \ No newline at end of file diff --git a/UICatalog/Scenarios/Images.cs b/UICatalog/Scenarios/Images.cs deleted file mode 100644 index d0c671b1b..000000000 --- a/UICatalog/Scenarios/Images.cs +++ /dev/null @@ -1,123 +0,0 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.ColorSpaces; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using Terminal.Gui; -using Attribute = Terminal.Gui.Attribute; - -namespace UICatalog.Scenarios { - [ScenarioMetadata (Name: "Images", Description: "Demonstration of how to render an image with/without true color support.")] - [ScenarioCategory ("Colors")] - public class Images : Scenario - { - public override void Setup () - { - base.Setup (); - - var x = 0; - var y = 0; - - var canTrueColor = Application.Driver.SupportsTrueColorOutput; - - var lblDriverName = new Label ($"Current driver is {Application.Driver.GetType ().Name}") { - X = x, - Y = y++ - }; - Win.Add (lblDriverName); - y++; - - var cbSupportsTrueColor = new CheckBox ("Driver supports true color ") { - X = x, - Y = y++, - Checked = canTrueColor, - CanFocus = false - }; - Win.Add (cbSupportsTrueColor); - - var cbUseTrueColor = new CheckBox ("Use true color") { - X = x, - Y = y++, - Checked = Application.Driver.UseTrueColor, - Enabled = canTrueColor, - }; - cbUseTrueColor.Toggled += (_) => Application.Driver.UseTrueColor = cbUseTrueColor.Checked; - Win.Add (cbUseTrueColor); - - var btnOpenImage = new Button ("Open Image") { - X = x, - Y = y++ - }; - Win.Add (btnOpenImage); - - var imageView = new ImageView () { - X = x, - Y = y++, - Width = Dim.Fill(), - Height = Dim.Fill(), - }; - Win.Add (imageView); - - btnOpenImage.Clicked += () => { - var ofd = new OpenDialog ("Open Image", "Image"); - Application.Run (ofd); - - var path = ofd.FilePath.ToString (); - - if (string.IsNullOrWhiteSpace (path)) { - return; - } - - if(!File.Exists(path)) { - return; - } - - imageView.SetImage(Image.Load (File.ReadAllBytes (path))); - }; - } - - class ImageView : View { - - private Image fullResImage; - private Image matchSize; - - ConcurrentDictionary cache = new ConcurrentDictionary(); - - internal void SetImage (Image image) - { - fullResImage = image; - this.SetNeedsDisplay (); - } - - public override void Redraw (Rect bounds) - { - base.Redraw (bounds); - - if (fullResImage == null) { - return; - } - - // if we have not got a cached resized image of this size - if(matchSize == null || bounds.Width != matchSize.Width || bounds.Height != matchSize.Height) { - - // generate one - matchSize = fullResImage.Clone (x => x.Resize (bounds.Width, bounds.Height)); - } - - for (int y = 0; y < bounds.Height; y++) { - for (int x = 0; x < bounds.Width; x++) { - var rgb = matchSize [x, y]; - - var attr = cache.GetOrAdd (rgb, (rgb) => new Attribute (new TrueColor (), new TrueColor (rgb.R, rgb.G, rgb.B))); - - Driver.SetAttribute(attr); - AddRune (x, y, ' '); - } - } - } - } - } -} \ No newline at end of file diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index 84f0cdb07..4cc046017 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -18,6 +18,9 @@ TRACE;DEBUG_IDISPOSABLE + + + From 2b0d14f41f96d5e0c14d2ef80141049b2d024d67 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 17 Feb 2023 23:12:48 +0000 Subject: [PATCH 09/12] Resizing --- UICatalog/Scenarios/Animation.cs | 36 +++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/UICatalog/Scenarios/Animation.cs b/UICatalog/Scenarios/Animation.cs index 231b0f399..ececdb508 100644 --- a/UICatalog/Scenarios/Animation.cs +++ b/UICatalog/Scenarios/Animation.cs @@ -134,11 +134,15 @@ namespace UICatalog.Scenarios { private Image[] fullResImages; private Image[] matchSizes; + Rect oldSize = Rect.Empty; + + internal void SetImage (Image image) { frameCount = image.Frames.Count; fullResImages = new Image[frameCount]; + matchSizes = new Image[frameCount]; for(int i=0;i[frameCount]; + oldSize = bounds; + } + + var imgScaled = matchSizes[currentFrame]; + + if(imgScaled == null) + { + var imgFull = fullResImages[currentFrame]; - var lines = GetBraille(); + // keep aspect ratio + var newSize = Math.Min(bounds.Width,bounds.Height); + + // generate one + matchSizes[currentFrame] = imgFull.Clone ( + x => x.Resize ( + newSize * BitmapToBraille.CHAR_HEIGHT, + newSize * BitmapToBraille.CHAR_HEIGHT)); + + } + + var lines = GetBraille(matchSizes[currentFrame]); for(int y = 0; y < lines.Length;y++) { @@ -169,10 +197,8 @@ namespace UICatalog.Scenarios { } } - private string[] GetBraille () + private string[] GetBraille (Image img) { - var img = fullResImages[currentFrame]; - var braille = new BitmapToBraille( img.Width, img.Height, From 8371a74bf94921939d5e394e0324219f0c8118f3 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 17 Feb 2023 23:18:20 +0000 Subject: [PATCH 10/12] CC attribution --- UICatalog/Scenarios/Animation.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/UICatalog/Scenarios/Animation.cs b/UICatalog/Scenarios/Animation.cs index ececdb508..783990e04 100644 --- a/UICatalog/Scenarios/Animation.cs +++ b/UICatalog/Scenarios/Animation.cs @@ -23,17 +23,24 @@ namespace UICatalog.Scenarios { { base.Setup (); - var x = 0; - var y = 0; var imageView = new ImageView () { - X = x, - Y = y++, Width = Dim.Fill(), - Height = Dim.Fill(), + Height = Dim.Fill()-2, }; + Win.Add (imageView); + var lbl = new Label("Image by Wikiscient"){ + Y = Pos.AnchorEnd(2) + }; + Win.Add(lbl); + + var lbl2 = new Label("https://commons.wikimedia.org/wiki/File:Spinning_globe.gif"){ + Y = Pos.AnchorEnd(1) + }; + Win.Add(lbl2); + var dir = new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); var f = new FileInfo( From 0e84090c31451fa4af82db7427c96d9a6ebb46a0 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 18 Feb 2023 07:56:45 +0000 Subject: [PATCH 11/12] Add caching and comments --- UICatalog/Scenarios/Animation.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/UICatalog/Scenarios/Animation.cs b/UICatalog/Scenarios/Animation.cs index 783990e04..6f25a293f 100644 --- a/UICatalog/Scenarios/Animation.cs +++ b/UICatalog/Scenarios/Animation.cs @@ -56,6 +56,7 @@ namespace UICatalog.Scenarios { Task.Run(()=>{ while(!isDisposed) { + // When updating from a Thread/Task always use Invoke Application.MainLoop.Invoke(()=> { imageView.NextFrame(); @@ -140,6 +141,7 @@ namespace UICatalog.Scenarios { private Image[] fullResImages; private Image[] matchSizes; + private string[] brailleCache; Rect oldSize = Rect.Empty; @@ -150,6 +152,7 @@ namespace UICatalog.Scenarios { fullResImages = new Image[frameCount]; matchSizes = new Image[frameCount]; + brailleCache = new string[frameCount]; for(int i=0;i[frameCount]; + brailleCache = new string[frameCount]; oldSize = bounds; } var imgScaled = matchSizes[currentFrame]; + var braille = brailleCache[currentFrame]; if(imgScaled == null) { @@ -185,14 +190,19 @@ namespace UICatalog.Scenarios { var newSize = Math.Min(bounds.Width,bounds.Height); // generate one - matchSizes[currentFrame] = imgFull.Clone ( + matchSizes[currentFrame] = imgScaled = imgFull.Clone ( x => x.Resize ( newSize * BitmapToBraille.CHAR_HEIGHT, newSize * BitmapToBraille.CHAR_HEIGHT)); - } - var lines = GetBraille(matchSizes[currentFrame]); + if(braille == null) + { + brailleCache[currentFrame] = braille = GetBraille(matchSizes[currentFrame]); + } + + + var lines = braille.Split('\n'); for(int y = 0; y < lines.Length;y++) { @@ -204,15 +214,14 @@ namespace UICatalog.Scenarios { } } - private string[] GetBraille (Image img) + private string GetBraille (Image img) { var braille = new BitmapToBraille( img.Width, img.Height, (x,y)=>IsLit(img,x,y)); - var pix = braille.GenerateImage(); - return pix.Split('\n'); + return braille.GenerateImage(); } private bool IsLit (Image img, int x, int y) From c85ff954aa8cebf0e0eb934d9d283551a6f63db8 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 20 Feb 2023 11:53:38 +0000 Subject: [PATCH 12/12] Illustrates #2331 (Scrollview not respecting clip) does not reproduce (#2332) * Proves that the issue #2331 don't have reason to happen. * fixes #2336 * Fixes #2331. ScrollView may not be honoring clip region; CustomButton shows outside * More appropriate solution for the issue #2331. * Start refactoring LineCanvas for mixing line style support (e.g. double into single) * Add remaining resolvers * Implement corner border style mixing in LineCanvas * Refactor and simplify resolvers * Move tests to Core folder and namespace to Terminal.Gui.CoreTests * Fixes #2333. TextField is selecting badly a word on double click. * Add unit test deleting a word with accented char. * Fixes 2331. ScrollView may not be honoring clip region. * Add a custom button scenario. * Fixes #2350. Clipping broke (see Clipping scenario). * Is preferable use NeedDisplay instead of Bounds. --------- Co-authored-by: Tig Kindel Co-authored-by: tznind --- Terminal.Gui/Core/TextFormatter.cs | 4 +- Terminal.Gui/Core/View.cs | 26 +- UICatalog/Scenarios/ASCIICustomButton.cs | 313 +++++++++++++++++++++++ UnitTests/Core/BorderTests.cs | 80 +++++- UnitTests/Views/ScrollViewTests.cs | 228 ++++++++++++++++- 5 files changed, 620 insertions(+), 31 deletions(-) create mode 100644 UICatalog/Scenarios/ASCIICustomButton.cs diff --git a/Terminal.Gui/Core/TextFormatter.cs b/Terminal.Gui/Core/TextFormatter.cs index 50fc9f5ae..2e5c84ebe 100644 --- a/Terminal.Gui/Core/TextFormatter.cs +++ b/Terminal.Gui/Core/TextFormatter.cs @@ -1190,7 +1190,9 @@ namespace Terminal.Gui { for (int line = 0; line < linesFormated.Count; line++) { if ((isVertical && line > bounds.Width) || (!isVertical && line > bounds.Height)) continue; - if ((isVertical && line > maxBounds.Left + maxBounds.Width - bounds.X) || (!isVertical && line > maxBounds.Top + maxBounds.Height - bounds.Y)) + if ((isVertical && line >= maxBounds.Left + maxBounds.Width - 1) + || (!isVertical && line >= maxBounds.Top + maxBounds.Height - 1)) + break; var runes = lines [line].ToRunes (); diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 96f58b217..adf368c81 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -1105,15 +1105,8 @@ namespace Terminal.Gui { /// public void Clear () { - Rect containerBounds = GetContainerBounds (); - Rect viewBounds = Bounds; - if (!containerBounds.IsEmpty) { - viewBounds.Width = Math.Min (viewBounds.Width, containerBounds.Width); - viewBounds.Height = Math.Min (viewBounds.Height, containerBounds.Height); - } - - var h = viewBounds.Height; - var w = viewBounds.Width; + var h = Frame.Height; + var w = Frame.Width; for (var line = 0; line < h; line++) { Move (0, line); for (var col = 0; col < w; col++) @@ -1525,13 +1518,13 @@ namespace Terminal.Gui { } if (!ustring.IsNullOrEmpty (TextFormatter.Text)) { - Clear (); + Rect containerBounds = GetContainerBounds (); + Clear (ViewToScreen (GetNeedDisplay (containerBounds))); SetChildNeedsDisplay (); // Draw any Text if (TextFormatter != null) { TextFormatter.NeedsFormat = true; } - Rect containerBounds = GetContainerBounds (); TextFormatter?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : GetNormalColor (), HasFocus ? ColorScheme.HotFocus : Enabled ? ColorScheme.HotNormal : ColorScheme.Disabled, containerBounds); @@ -1569,6 +1562,17 @@ namespace Terminal.Gui { ClearNeedsDisplay (); } + Rect GetNeedDisplay (Rect containerBounds) + { + Rect rect = NeedDisplay; + if (!containerBounds.IsEmpty) { + rect.Width = Math.Min (NeedDisplay.Width, containerBounds.Width); + rect.Height = Math.Min (NeedDisplay.Height, containerBounds.Height); + } + + return rect; + } + Rect GetContainerBounds () { var containerBounds = SuperView == null ? default : SuperView.ViewToScreen (SuperView.Bounds); diff --git a/UICatalog/Scenarios/ASCIICustomButton.cs b/UICatalog/Scenarios/ASCIICustomButton.cs new file mode 100644 index 000000000..77cacf8b9 --- /dev/null +++ b/UICatalog/Scenarios/ASCIICustomButton.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Terminal.Gui; + +namespace UICatalog.Scenarios { + [ScenarioMetadata (Name: "ASCIICustomButtonTest", Description: "ASCIICustomButton sample")] + [ScenarioCategory ("Controls")] + public class ASCIICustomButtonTest : Scenario { + private static bool smallerWindow; + private ScrollViewTestWindow scrollViewTestWindow; + private MenuItem miSmallerWindow; + + public override void Init (ColorScheme colorScheme) + { + Application.Init (); + scrollViewTestWindow = new ScrollViewTestWindow (); + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem("Window Size", new MenuItem [] { + miSmallerWindow = new MenuItem ("Smaller Window", "", ChangeWindowSize) { + CheckType = MenuItemCheckStyle.Checked + }, + null, + new MenuItem("Quit", "",() => Application.RequestStop(),null,null, Key.Q | Key.CtrlMask) + }) + }); + Application.Top.Add (menu, scrollViewTestWindow); + Application.Run (); + } + + private void ChangeWindowSize () + { + smallerWindow = miSmallerWindow.Checked = !miSmallerWindow.Checked; + scrollViewTestWindow.Dispose (); + Application.Top.Remove (scrollViewTestWindow); + scrollViewTestWindow = new ScrollViewTestWindow (); + Application.Top.Add (scrollViewTestWindow); + } + + public override void Run () + { + } + + public class ASCIICustomButton : Button { + public string Description => $"Description of: {id}"; + + public event Action PointerEnter; + + private Label fill; + private FrameView border; + private string id; + + public ASCIICustomButton (string text, Pos x, Pos y, int width, int height) : base (text) + { + CustomInitialize ("", text, x, y, width, height); + } + + public ASCIICustomButton (string id, string text, Pos x, Pos y, int width, int height) : base (text) + { + CustomInitialize (id, text, x, y, width, height); + } + + private void CustomInitialize (string id, string text, Pos x, Pos y, int width, int height) + { + this.id = id; + X = x; + Y = y; + + Frame = new Rect { + Width = width, + Height = height + }; + + border = new FrameView () { + Width = width, + Height = height + }; + + AutoSize = false; + + var fillText = new System.Text.StringBuilder (); + for (int i = 0; i < Bounds.Height; i++) { + if (i > 0) { + fillText.AppendLine (""); + } + for (int j = 0; j < Bounds.Width; j++) { + fillText.Append ("█"); + } + } + + fill = new Label (fillText.ToString ()) { + Visible = false, + CanFocus = false + }; + + var title = new Label (text) { + X = Pos.Center (), + Y = Pos.Center (), + }; + + border.MouseClick += This_MouseClick; + border.Subviews [0].MouseClick += This_MouseClick; + fill.MouseClick += This_MouseClick; + title.MouseClick += This_MouseClick; + + Add (border, fill, title); + } + + private void This_MouseClick (MouseEventArgs obj) + { + OnMouseEvent (obj.MouseEvent); + } + + public override bool OnMouseEvent (MouseEvent mouseEvent) + { + Debug.WriteLine ($"{mouseEvent.Flags}"); + if (mouseEvent.Flags == MouseFlags.Button1Clicked) { + if (!HasFocus && SuperView != null) { + if (!SuperView.HasFocus) { + SuperView.SetFocus (); + } + SetFocus (); + SetNeedsDisplay (); + } + + OnClicked (); + return true; + } + return base.OnMouseEvent (mouseEvent); + } + + public override bool OnEnter (View view) + { + border.Visible = false; + fill.Visible = true; + PointerEnter.Invoke (this); + view = this; + return base.OnEnter (view); + } + + public override bool OnLeave (View view) + { + border.Visible = true; + fill.Visible = false; + if (view == null) + view = this; + return base.OnLeave (view); + } + } + + public class ScrollViewTestWindow : Window { + private List