Browse Source

Added Box element, Redesigned Debug element, Added image placeholder

Marcin Ziąbek 4 years ago
parent
commit
f77d8baa63

+ 1 - 1
QuestPDF.Examples/ElementExamples.cs

@@ -38,7 +38,7 @@ namespace QuestPDF.Examples
                         .Background("#DDD")
                         .Background("#DDD")
                         .Padding(10)
                         .Padding(10)
                         .ExtendVertical()
                         .ExtendVertical()
-                        .Text(TextPlaceholder.LoremIpsum());
+                        .Text(Helpers.Placeholders.LoremIpsum());
                 });
                 });
         }
         }
         
         

+ 2 - 2
QuestPDF.Examples/FrameExample.cs

@@ -34,8 +34,8 @@ namespace QuestPDF.Examples
                     {
                     {
                         stack.Element().Row(row =>
                         stack.Element().Row(row =>
                         {
                         {
-                            row.RelativeColumn(2).LabelCell().Text(TextPlaceholder.Label());
-                            row.RelativeColumn(3).ValueCell().Text(TextPlaceholder.Paragraph());
+                            row.RelativeColumn(2).LabelCell().Text(Placeholders.Label());
+                            row.RelativeColumn(3).ValueCell().Text(Placeholders.Paragraph());
                         });
                         });
                     }
                     }
                 });
                 });

+ 4 - 4
QuestPDF.ReportSample/DataSource.cs

@@ -52,7 +52,7 @@ namespace QuestPDF.ReportSample
                 
                 
                 return new ReportSection
                 return new ReportSection
                 {
                 {
-                    Title = TextPlaceholder.Label(),
+                    Title = Placeholders.Label(),
                     Parts = Enumerable.Range(0, sectionLength).Select(x => GetRandomElement()).ToList()
                     Parts = Enumerable.Range(0, sectionLength).Select(x => GetRandomElement()).ToList()
                 };
                 };
             }
             }
@@ -74,8 +74,8 @@ namespace QuestPDF.ReportSample
             {
             {
                 return new ReportSectionText
                 return new ReportSectionText
                 {
                 {
-                    Label = TextPlaceholder.Label(),
-                    Text = TextPlaceholder.Paragraph()
+                    Label = Placeholders.Label(),
+                    Text = Placeholders.Paragraph()
                 };
                 };
             }
             }
             
             
@@ -113,7 +113,7 @@ namespace QuestPDF.ReportSample
                 {
                 {
                     PhotoData = Helpers.GetPhoto($"{photoId}.jpg"),
                     PhotoData = Helpers.GetPhoto($"{photoId}.jpg"),
 
 
-                    Comments = TextPlaceholder.Paragraph(),
+                    Comments = Placeholders.Paragraph(),
                     Date = DateTime.Now - TimeSpan.FromDays(Helpers.Random.NextDouble() * 100),
                     Date = DateTime.Now - TimeSpan.FromDays(Helpers.Random.NextDouble() * 100),
                     Location = Helpers.RandomLocation(),
                     Location = Helpers.RandomLocation(),
 
 

+ 3 - 3
QuestPDF.ReportSample/Layouts/PhotoTemplate.cs

@@ -30,14 +30,14 @@ namespace QuestPDF.ReportSample.Layouts
             container
             container
                 .Row(row =>
                 .Row(row =>
                 {
                 {
-                    row.RelativeColumn(2).Component(new ImageTemplate(Model.PhotoData));
+                    row.RelativeColumn(2).AspectRatio(4 / 3f).Image(Placeholders.Image);
 
 
                     row.RelativeColumn().PaddingLeft(5).Stack(stack =>
                     row.RelativeColumn().PaddingLeft(5).Stack(stack =>
                     {
                     {
                         stack.Spacing(7f);
                         stack.Spacing(7f);
                         
                         
-                        stack.Element().Component(new ImageTemplate(Model.MapDetailsSource));
-                        stack.Element().Component(new ImageTemplate(Model.MapContextSource));
+                        stack.Element().AspectRatio(4 / 3f).Image(Placeholders.Image);
+                        stack.Element().AspectRatio(4 / 3f).Image(Placeholders.Image);
                     });
                     });
                 });
                 });
         }
         }

+ 3 - 3
QuestPDF.ReportSample/Layouts/SectionTemplate.cs

@@ -59,7 +59,7 @@ namespace QuestPDF.ReportSample.Layouts
             {
             {
                 stack.Spacing(5);
                 stack.Spacing(5);
                 
                 
-                stack.Element().Component(new ImageTemplate(model.ImageSource));
+                stack.Element().MaxWidth(250).AspectRatio(4 / 3f).Image(Placeholders.Image);
                 stack.Element().Text(model.Location.Format(), Typography.Normal);
                 stack.Element().Text(model.Location.Format(), Typography.Normal);
             });
             });
         }
         }
@@ -72,12 +72,12 @@ namespace QuestPDF.ReportSample.Layouts
                 return;
                 return;
             }
             }
 
 
-            container.Debug().Debug().Grid(grid =>
+            container.Debug("Photo gallery").Grid(grid =>
             {
             {
                 grid.Spacing(5);
                 grid.Spacing(5);
                 grid.Columns(3);
                 grid.Columns(3);
                 
                 
-                model.Photos.ForEach(x => grid.Element().Component(new ImageTemplate(x)));
+                model.Photos.ForEach(x => grid.Element().AspectRatio(4 / 3f).Image(Placeholders.Image));
             });
             });
         }
         }
     }
     }

+ 1 - 1
QuestPDF.ReportSample/Tests.cs

@@ -26,7 +26,7 @@ namespace QuestPDF.ReportSample
         public void PerformanceBenchmark()
         public void PerformanceBenchmark()
         {
         {
             // test size
             // test size
-            const int testSize = 250;
+            const int testSize = 100;
             const decimal performanceTarget = 5; // documents per second
             const decimal performanceTarget = 5; // documents per second
 
 
             // create report models
             // create report models

+ 5 - 4
QuestPDF.UnitTests/BackgroundTests.cs

@@ -1,5 +1,6 @@
 using NUnit.Framework;
 using NUnit.Framework;
 using QuestPDF.Elements;
 using QuestPDF.Elements;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.UnitTests.TestEngine;
 using QuestPDF.UnitTests.TestEngine;
 
 
@@ -17,10 +18,10 @@ namespace QuestPDF.UnitTests
             TestPlan
             TestPlan
                 .For(x => new Background
                 .For(x => new Background
                 {
                 {
-                    Color = "#F00"
+                    Color = Colors.Red.Medium
                 })
                 })
                 .DrawElement(new Size(400, 300))
                 .DrawElement(new Size(400, 300))
-                .ExpectCanvasDrawRectangle(new Position(0, 0), new Size(400, 300), "#F00")
+                .ExpectCanvasDrawRectangle(new Position(0, 0), new Size(400, 300), Colors.Red.Medium)
                 .CheckDrawResult();
                 .CheckDrawResult();
         }
         }
         
         
@@ -30,11 +31,11 @@ namespace QuestPDF.UnitTests
             TestPlan
             TestPlan
                 .For(x => new Background
                 .For(x => new Background
                 {
                 {
-                    Color = "#F00",
+                    Color = Colors.Red.Medium,
                     Child = x.CreateChild()
                     Child = x.CreateChild()
                 })
                 })
                 .DrawElement(new Size(400, 300))
                 .DrawElement(new Size(400, 300))
-                .ExpectCanvasDrawRectangle(new Position(0, 0), new Size(400, 300), "#F00")
+                .ExpectCanvasDrawRectangle(new Position(0, 0), new Size(400, 300), Colors.Red.Medium)
                 .ExpectChildDraw(new Size(400, 300))
                 .ExpectChildDraw(new Size(400, 300))
                 .CheckDrawResult();
                 .CheckDrawResult();
         }
         }

+ 6 - 5
QuestPDF.UnitTests/BorderTests.cs

@@ -1,6 +1,7 @@
 using NUnit.Framework;
 using NUnit.Framework;
 using QuestPDF.Drawing.SpacePlan;
 using QuestPDF.Drawing.SpacePlan;
 using QuestPDF.Elements;
 using QuestPDF.Elements;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.UnitTests.TestEngine;
 using QuestPDF.UnitTests.TestEngine;
 
 
@@ -41,16 +42,16 @@ namespace QuestPDF.UnitTests
                     Bottom = 30,
                     Bottom = 30,
                     Left = 40,
                     Left = 40,
                     
                     
-                    Color = "#FF0000",
+                    Color = Colors.Red.Medium,
                     
                     
                     Child = x.CreateChild()
                     Child = x.CreateChild()
                 })
                 })
                 .DrawElement(new Size(400, 300))
                 .DrawElement(new Size(400, 300))
                 .ExpectChildDraw(new Size(400, 300))
                 .ExpectChildDraw(new Size(400, 300))
-                .ExpectCanvasDrawRectangle(new Position(-20, -5), new Size(430, 10), "#FF0000") // top
-                .ExpectCanvasDrawRectangle(new Position(-20, -5), new Size(40, 320), "#FF0000") // left
-                .ExpectCanvasDrawRectangle(new Position(-20, 285), new Size(430, 30), "#FF0000") // bottom
-                .ExpectCanvasDrawRectangle(new Position(390, -5), new Size(20, 320), "#FF0000") // right
+                .ExpectCanvasDrawRectangle(new Position(-20, -5), new Size(430, 10), Colors.Red.Medium) // top
+                .ExpectCanvasDrawRectangle(new Position(-20, -5), new Size(40, 320), Colors.Red.Medium) // left
+                .ExpectCanvasDrawRectangle(new Position(-20, 285), new Size(430, 30), Colors.Red.Medium) // bottom
+                .ExpectCanvasDrawRectangle(new Position(390, -5), new Size(20, 320), Colors.Red.Medium) // right
                 .ExpectChildDraw(new Size(400, 300))
                 .ExpectChildDraw(new Size(400, 300))
                 .CheckDrawResult();
                 .CheckDrawResult();
         }
         }

+ 1 - 2
QuestPDF.UnitTests/DebugTests.cs

@@ -7,7 +7,6 @@ namespace QuestPDF.UnitTests
     [TestFixture]
     [TestFixture]
     public class DebugTests
     public class DebugTests
     {
     {
-        [Test]
-        public void Measure_ShouldHandleNullChild() => new Debug().MeasureWithoutChild();
+        
     }
     }
 }
 }

+ 2 - 1
QuestPDF/Elements/Background.cs

@@ -1,10 +1,11 @@
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Elements
 namespace QuestPDF.Elements
 {
 {
     internal class Background : ContainerElement
     internal class Background : ContainerElement
     {
     {
-        public string Color { get; set; } = "#00000000";
+        public string Color { get; set; } = Colors.Black;
         
         
         internal override void Draw(ICanvas canvas, Size availableSpace)
         internal override void Draw(ICanvas canvas, Size availableSpace)
         {
         {

+ 2 - 1
QuestPDF/Elements/Border.cs

@@ -1,10 +1,11 @@
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Elements
 namespace QuestPDF.Elements
 {
 {
     internal class Border : ContainerElement
     internal class Border : ContainerElement
     {
     {
-        public string Color { get; set; } = "#000000";
+        public string Color { get; set; } = Colors.Black;
 
 
         public float Top { get; set; }
         public float Top { get; set; }
         public float Right { get; set; }
         public float Right { get; set; }

+ 17 - 0
QuestPDF/Elements/Box.cs

@@ -0,0 +1,17 @@
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements
+{
+    internal class Box : ContainerElement
+    {
+        internal override void Draw(ICanvas canvas, Size availableSpace)
+        {
+            var targetSize = Child?.Measure(availableSpace) as Size;
+            
+            if (targetSize == null)
+                return;
+            
+            Child?.Draw(canvas, targetSize);
+        }
+    }
+}

+ 25 - 26
QuestPDF/Elements/Debug.cs

@@ -1,37 +1,36 @@
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
+using SkiaSharp;
 
 
 namespace QuestPDF.Elements
 namespace QuestPDF.Elements
 {
 {
-    internal class Debug : ContainerElement
+    internal class Debug : IComponent
     {
     {
-        private static readonly TextStyle TextStyle = TextStyle.Default.Color("#F00").FontType("Consolas").Size(10);
+        public IElement? Child { get; set; }
         
         
-        internal override void Draw(ICanvas canvas, Size availableSpace)
+        public string Text { get; set; }
+        public string Color { get; set; } = Colors.Red.Medium;
+        public void Compose(IContainer container)
         {
         {
-            Child?.Draw(canvas, availableSpace);
-            DrawBoundingBox();
-            DrawDimensions();
-                
-            void DrawBoundingBox()
-            {
-                // TODO: when layer element is done, move this code into fluent API
-                
-                var container = new Container();
-
-                container
-                    .Border(1)
-                    .BorderColor("#FF0000")
-                    .Background("#33FF0000");
-
-                container.Draw(canvas, availableSpace);
-            }
-
-            void DrawDimensions()
-            {
-                canvas.DrawText($"W: {availableSpace.Width:F1}", new Position(5, 12), TextStyle);
-                canvas.DrawText($"H: {availableSpace.Height:F1}", new Position(5, 22), TextStyle);
-            }
+            var backgroundColor = SKColor.Parse(Color).WithAlpha(75).ToString();
+            
+            container
+                .Border(1)
+                .BorderColor(Color)
+                .Layers(layers =>
+                {
+                    layers.PrimaryLayer().Element(Child);
+                    layers.Layer().Background(backgroundColor);
+                    
+                    layers
+                        .Layer()
+                        .ShowIf(!string.IsNullOrWhiteSpace(Text))
+                        .Box()
+                        .Background(Colors.White)
+                        .Padding(2)
+                        .Text(Text, TextStyle.Default.Color(Color).FontType("Consolas").Size(8));
+                });
         }
         }
     }
     }
 }
 }

+ 11 - 2
QuestPDF/Elements/Placeholder.cs

@@ -1,10 +1,12 @@
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Elements
 namespace QuestPDF.Elements
 {
 {
     internal class Placeholder : IComponent
     internal class Placeholder : IComponent
     {
     {
+        public string Text { get; set; }
         private static readonly byte[] ImageData;
         private static readonly byte[] ImageData;
 
 
         static Placeholder()
         static Placeholder()
@@ -15,11 +17,18 @@ namespace QuestPDF.Elements
         public void Compose(IContainer container)
         public void Compose(IContainer container)
         {
         {
             container
             container
-                .Background("CCC")
+                .Background(Colors.Grey.Lighten2)
                 .AlignMiddle()
                 .AlignMiddle()
                 .AlignCenter()
                 .AlignCenter()
+                .Padding(5)
                 .MaxHeight(32)
                 .MaxHeight(32)
-                .Image(ImageData, ImageScaling.FitArea);
+                .Element(x =>
+                {
+                    if (string.IsNullOrWhiteSpace(Text))
+                        x.Image(ImageData, ImageScaling.FitArea);
+                    else
+                        x.Text(Text, TextStyle.Default.Size(14).SemiBold());
+                });
         }
         }
     }
     }
 }
 }

+ 33 - 0
QuestPDF/Fluent/DebugExtensions.cs

@@ -0,0 +1,33 @@
+using QuestPDF.Elements;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Fluent
+{
+    public static class DebugExtensions
+    {
+        public static IContainer Debug(this IContainer parent, string text, string color)
+        {
+            var container = new Container();
+
+            parent.Component(new Debug
+            {
+                Child = container,
+                Text = text,
+                Color = color
+            });
+
+            return container;
+        }
+        
+        public static IContainer Debug(this IContainer parent, string text)
+        {
+            return parent.Debug(text, Colors.Red.Medium);
+        }
+
+        public static IContainer Debug(this IContainer parent)
+        {
+            return parent.Debug(string.Empty, Colors.Red.Medium);
+        }
+    }
+}

+ 12 - 8
QuestPDF/Fluent/ElementExtensions.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Elements;
 using QuestPDF.Elements;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Fluent
 namespace QuestPDF.Fluent
@@ -40,11 +41,6 @@ namespace QuestPDF.Fluent
         {
         {
             return handler(parent);
             return handler(parent);
         }
         }
-        
-        public static IContainer Debug(this IContainer parent)
-        {
-            return parent.Element(new Debug());
-        }
 
 
         public static void PageNumber(this IContainer element, string textFormat = "{number}", TextStyle? style = null)
         public static void PageNumber(this IContainer element, string textFormat = "{number}", TextStyle? style = null)
         {
         {
@@ -71,10 +67,13 @@ namespace QuestPDF.Fluent
                 Color = color
                 Color = color
             });
             });
         }
         }
-        
-        public static void Placeholder(this IContainer element)
+
+        public static void Placeholder(this IContainer element, string? text = null)
         {
         {
-            element.Component<Placeholder>();
+            element.Component(new Elements.Placeholder
+            {
+                Text = text ?? string.Empty
+            });
         }
         }
 
 
         public static IContainer ShowOnce(this IContainer element)
         public static IContainer ShowOnce(this IContainer element)
@@ -159,5 +158,10 @@ namespace QuestPDF.Fluent
                 Handler = handler
                 Handler = handler
             });
             });
         }
         }
+        
+        public static IContainer Box(this IContainer element)
+        {
+            return element.Element(new Box());
+        }
     }
     }
 }
 }

+ 1 - 1
QuestPDF/Fluent/ImageExtensions.cs

@@ -37,7 +37,7 @@ namespace QuestPDF.Fluent
             }
             }
         }
         }
 
 
-        public static void DynamicImage(this IContainer element, Func<Size, byte[]> imageSource)
+        public static void Image(this IContainer element, Func<Size, byte[]> imageSource)
         {
         {
             element.Element(new DynamicImage
             element.Element(new DynamicImage
             {
             {

+ 125 - 1
QuestPDF/Helpers/TextPlaceholder.cs → QuestPDF/Helpers/Placeholders.cs

@@ -1,9 +1,12 @@
 using System;
 using System;
 using System.Linq;
 using System.Linq;
+using QuestPDF.Drawing;
+using QuestPDF.Infrastructure;
+using SkiaSharp;
 
 
 namespace QuestPDF.Helpers
 namespace QuestPDF.Helpers
 {
 {
-    public static class TextPlaceholder
+    public static class Placeholders
     {
     {
         private static Random Random = new Random();
         private static Random Random = new Random();
         
         
@@ -148,5 +151,126 @@ namespace QuestPDF.Helpers
         public static string Percent() => (Random.NextDouble() * 100).ToString("N0") + "%";
         public static string Percent() => (Random.NextDouble() * 100).ToString("N0") + "%";
 
 
         #endregion
         #endregion
+
+        #region Visual
+
+        private static readonly string[] BackgroundColors =
+        {
+            Colors.Red.Lighten2,
+            Colors.Pink.Lighten2,
+            Colors.Purple.Lighten2,
+            Colors.DeepPurple.Lighten2,
+            Colors.Indigo.Lighten2,
+            Colors.Blue.Lighten2,
+            Colors.LightBlue.Lighten2,
+            Colors.Cyan.Lighten2,
+            Colors.Teal.Lighten2,
+            Colors.Green.Lighten2,
+            Colors.LightGreen.Lighten2,
+            Colors.Lime.Lighten2,
+            Colors.Yellow.Lighten2,
+            Colors.Amber.Lighten2,
+            Colors.Orange.Lighten2,
+            Colors.DeepOrange.Lighten2,
+            Colors.Brown.Lighten2,
+            Colors.Grey.Lighten2,
+            Colors.BlueGrey.Lighten2
+        };
+        
+        public static string BackgroundColor()
+        {
+            var index = Random.Next(0, BackgroundColors.Length);
+            return BackgroundColors[index];
+        }
+        
+        public static string Color()
+        {
+            var colors = new[]
+            {
+                Colors.Red.Medium,
+                Colors.Pink.Medium,
+                Colors.Purple.Medium,
+                Colors.DeepPurple.Medium,
+                Colors.Indigo.Medium,
+                Colors.Blue.Medium,
+                Colors.LightBlue.Medium,
+                Colors.Cyan.Medium,
+                Colors.Teal.Medium,
+                Colors.Green.Medium,
+                Colors.LightGreen.Medium,
+                Colors.Lime.Medium,
+                Colors.Yellow.Medium,
+                Colors.Amber.Medium,
+                Colors.Orange.Medium,
+                Colors.DeepOrange.Medium,
+                Colors.Brown.Medium,
+                Colors.Grey.Medium,
+                Colors.BlueGrey.Medium
+            };
+
+            var index = Random.Next(0, colors.Length);
+            return colors[index];
+        }
+        
+        public static byte[] Image(Size size)
+        {
+            // shuffle corner positions
+            var targetPositions = new[]
+            {
+                new SKPoint(0, 0),
+                new SKPoint(size.Width, 0),
+                new SKPoint(0, size.Height),
+                new SKPoint(size.Width, size.Height)
+            };
+            
+            var positions = targetPositions
+                .OrderBy(x => Random.Next())
+                .ToList();
+            
+            // rand and shuffle colors
+            var colors = BackgroundColors
+                .OrderBy(x => Random.Next())
+                .Take(4)
+                .Select(SKColor.Parse)
+                .ToArray();
+            
+            // create image with white background
+            var imageInfo = new SKImageInfo((int)size.Width, (int)size.Height);
+            using var surface = SKSurface.Create(imageInfo);
+   
+            using var backgroundPaint = new SKPaint
+            {
+                Color = SKColors.White
+            };
+            
+            surface.Canvas.DrawRect(0, 0, size.Width, size.Height, backgroundPaint);
+
+            // draw gradient
+            SKShader GetForegroundShader(int index)
+            {
+                var radius = Math.Max(size.Width, size.Height);
+                var color = colors[index];
+                
+                return SKShader.CreateRadialGradient(
+                    positions[index], radius,
+                    new[] {color, color.WithAlpha(0)}, new[] {0, 1f},
+                    SKShaderTileMode.Decal);
+            }
+            
+            using var shaderPaint = new SKPaint
+            {
+                Shader = SKShader.CreateCompose(
+                    SKShader.CreateCompose(GetForegroundShader(0), GetForegroundShader(1)),
+                    SKShader.CreateCompose(GetForegroundShader(2), GetForegroundShader(3)))
+            };
+            
+            surface.Canvas.DrawRect(0, 0, size.Width, size.Height, shaderPaint);
+            
+            // return result as an image
+            surface.Canvas.Save();
+            return surface.Snapshot().Encode(SKEncodedImageFormat.Jpeg, 90).ToArray();
+        }
+        
+        #endregion
     }
     }
 }
 }

+ 5 - 3
QuestPDF/Infrastructure/TextStyle.cs

@@ -1,9 +1,11 @@
-namespace QuestPDF.Infrastructure
+using QuestPDF.Helpers;
+
+namespace QuestPDF.Infrastructure
 {
 {
     public class TextStyle
     public class TextStyle
     {
     {
-        internal string Color { get; set; } = "#000000";
-        internal string FontType { get; set; } = "Helvetica";
+        internal string Color { get; set; } = Colors.Black;
+        internal string FontType { get; set; } = "Calibri";
         internal float Size { get; set; } = 12;
         internal float Size { get; set; } = 12;
         internal float LineHeight { get; set; } = 1.2f;
         internal float LineHeight { get; set; } = 1.2f;
         internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
         internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;