using System; using System.Collections.Generic; using System.Linq; using System.Text; using Terminal.Gui; namespace UICatalog.Scenarios; public interface ITool { void OnMouseEvent (DrawingArea area, MouseEvent mouseEvent); } internal class DrawLineTool : ITool { private StraightLine _currentLine; public LineStyle LineStyle { get; set; } = LineStyle.Single; /// public void OnMouseEvent (DrawingArea area, MouseEvent mouseEvent) { if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) { if (_currentLine == null) { // Mouse pressed down _currentLine = new ( mouseEvent.Position, 0, Orientation.Vertical, LineStyle, area.CurrentAttribute ); area.CurrentLayer.AddLine (_currentLine); } else { // Mouse dragged Point start = _currentLine.Start; Point end = mouseEvent.Position; var orientation = Orientation.Vertical; int length = end.Y - start.Y; // if line is wider than it is tall switch to horizontal if (Math.Abs (start.X - end.X) > Math.Abs (start.Y - end.Y)) { orientation = Orientation.Horizontal; length = end.X - start.X; } if (length > 0) { length++; } else { length--; } _currentLine.Length = length; _currentLine.Orientation = orientation; area.CurrentLayer.ClearCache (); area.SetNeedsDisplay (); } } else { // Mouse released if (_currentLine != null) { if (_currentLine.Length == 0) { _currentLine.Length = 1; } if (_currentLine.Style == LineStyle.None) { // Treat none as eraser int idx = area.Layers.IndexOf (area.CurrentLayer); area.Layers.Remove (area.CurrentLayer); area.CurrentLayer = new ( area.CurrentLayer.Lines.Exclude ( _currentLine.Start, _currentLine.Length, _currentLine.Orientation ) ); area.Layers.Insert (idx, area.CurrentLayer); } _currentLine = null; area.ClearUndo (); area.SetNeedsDisplay (); } } } } [ScenarioMetadata ("Line Drawing", "Demonstrates LineCanvas.")] [ScenarioCategory ("Controls")] [ScenarioCategory ("Drawing")] public class LineDrawing : Scenario { public override void Main () { Application.Init (); var win = new Window { Title = GetQuitKeyAndName () }; var canvas = new DrawingArea { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () }; var tools = new ToolsView { Title = "Tools", X = Pos.Right (canvas) - 20, Y = 2 }; tools.ColorChanged += (s, e) => canvas.SetAttribute (e); tools.SetStyle += b => canvas.CurrentTool = new DrawLineTool { LineStyle = b }; tools.AddLayer += () => canvas.AddLayer (); win.Add (canvas); win.Add (tools); tools.CurrentColor = canvas.GetNormalColor (); canvas.CurrentAttribute = tools.CurrentColor; win.KeyDown += (s, e) => { e.Handled = canvas.OnKeyDown (e); }; Application.Run (win); win.Dispose (); Application.Shutdown (); } public static bool PromptForColor (string title, Color current, out Color newColor) { var accept = false; var d = new Dialog { Title = title, Width = Application.Force16Colors ? 35 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)), Height = 10 }; var btnOk = new Button { X = Pos.Center () - 5, Y = Application.Force16Colors ? 6 : 4, Text = "Ok", Width = Dim.Auto (), IsDefault = true }; btnOk.Accepting += (s, e) => { accept = true; e.Cancel = true; Application.RequestStop (); }; var btnCancel = new Button { X = Pos.Center () + 5, Y = 4, Text = "Cancel", Width = Dim.Auto () }; btnCancel.Accepting += (s, e) => { e.Cancel = true; Application.RequestStop (); }; d.Add (btnOk); d.Add (btnCancel); d.AddButton (btnOk); d.AddButton (btnCancel); View cp; if (Application.Force16Colors) { cp = new ColorPicker16 { SelectedColor = current.GetClosestNamedColor16 (), Width = Dim.Fill () }; } else { cp = new ColorPicker { SelectedColor = current, Width = Dim.Fill (), Style = new () { ShowColorName = true, ShowTextFields = true } }; ((ColorPicker)cp).ApplyStyleChanges (); } d.Add (cp); Application.Run (d); d.Dispose (); newColor = Application.Force16Colors ? ((ColorPicker16)cp).SelectedColor : ((ColorPicker)cp).SelectedColor; return accept; } } public class ToolsView : Window { private Button _addLayerBtn; private readonly AttributeView _colors; private RadioGroup _stylePicker; public Attribute CurrentColor { get => _colors.Value; set => _colors.Value = value; } public ToolsView () { BorderStyle = LineStyle.Dotted; Border.Thickness = new (1, 2, 1, 1); Initialized += ToolsView_Initialized; _colors = new (); } public event Action AddLayer; public override void BeginInit () { base.BeginInit (); _colors.ValueChanged += (s, e) => ColorChanged?.Invoke (this, e); _stylePicker = new() { X = 0, Y = Pos.Bottom (_colors), RadioLabels = Enum.GetNames (typeof (LineStyle)).ToArray () }; _stylePicker.SelectedItemChanged += (s, a) => { SetStyle?.Invoke ((LineStyle)a.SelectedItem); }; _stylePicker.SelectedItem = 1; _addLayerBtn = new() { Text = "New Layer", X = Pos.Center (), Y = Pos.Bottom (_stylePicker) }; _addLayerBtn.Accepting += (s, a) => AddLayer?.Invoke (); Add (_colors, _stylePicker, _addLayerBtn); } public event EventHandler ColorChanged; public event Action SetStyle; private void ToolsView_Initialized (object sender, EventArgs e) { LayoutSubviews (); Width = Math.Max (_colors.Frame.Width, _stylePicker.Frame.Width) + GetAdornmentsThickness ().Horizontal; Height = _colors.Frame.Height + _stylePicker.Frame.Height + _addLayerBtn.Frame.Height + GetAdornmentsThickness ().Vertical; SuperView.LayoutSubviews (); } } public class DrawingArea : View { public readonly List Layers = new (); private readonly Stack _undoHistory = new (); public Attribute CurrentAttribute { get; set; } public LineCanvas CurrentLayer { get; set; } public ITool CurrentTool { get; set; } = new DrawLineTool (); public DrawingArea () { AddLayer (); } public override void OnDrawContentComplete (Rectangle viewport) { base.OnDrawContentComplete (viewport); foreach (LineCanvas canvas in Layers) { foreach (KeyValuePair c in canvas.GetCellMap ()) { if (c.Value is { }) { Driver.SetAttribute (c.Value.Value.Attribute ?? ColorScheme.Normal); // TODO: #2616 - Support combining sequences that don't normalize AddRune (c.Key.X, c.Key.Y, c.Value.Value.Rune); } } } } //// BUGBUG: Why is this not handled by a key binding??? public override bool OnKeyDown (Key e) { // BUGBUG: These should be implemented with key bindings if (e.KeyCode == (KeyCode.Z | KeyCode.CtrlMask)) { StraightLine pop = CurrentLayer.RemoveLastLine (); if (pop != null) { _undoHistory.Push (pop); SetNeedsDisplay (); return true; } } if (e.KeyCode == (KeyCode.Y | KeyCode.CtrlMask)) { if (_undoHistory.Any ()) { StraightLine pop = _undoHistory.Pop (); CurrentLayer.AddLine (pop); SetNeedsDisplay (); return true; } } return false; } protected override bool OnMouseEvent (MouseEvent mouseEvent) { CurrentTool.OnMouseEvent (this, mouseEvent); return base.OnMouseEvent (mouseEvent); } internal void AddLayer () { CurrentLayer = new (); Layers.Add (CurrentLayer); } internal void SetAttribute (Attribute a) { CurrentAttribute = a; } public void ClearUndo () { _undoHistory.Clear (); } } public class AttributeView : View { public event EventHandler ValueChanged; private Attribute _value; public Attribute Value { get => _value; set { _value = value; ValueChanged?.Invoke (this, value); } } private static readonly HashSet<(int, int)> ForegroundPoints = new() { (0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1) }; private static readonly HashSet<(int, int)> BackgroundPoints = new() { (3, 1), (1, 2), (2, 2), (3, 2) }; public AttributeView () { Width = 4; Height = 3; } /// public override void OnDrawContent (Rectangle viewport) { base.OnDrawContent (viewport); Color fg = Value.Foreground; Color bg = Value.Background; bool isTransparentFg = fg == GetNormalColor ().Background; bool isTransparentBg = bg == GetNormalColor ().Background; Driver.SetAttribute (new (fg, isTransparentFg ? Color.Gray : fg)); // Square of foreground color foreach ((int, int) point in ForegroundPoints) { // Make pattern like this when it is same color as background of control /*▓▒ ▒▓*/ Rune rune; if (isTransparentFg) { rune = (Rune)(point.Item1 % 2 == point.Item2 % 2 ? '▓' : '▒'); } else { rune = (Rune)'█'; } AddRune (point.Item1, point.Item2, rune); } Driver.SetAttribute (new (bg, isTransparentBg ? Color.Gray : bg)); // Square of background color foreach ((int, int) point in BackgroundPoints) { // Make pattern like this when it is same color as background of control /*▓▒ ▒▓*/ Rune rune; if (isTransparentBg) { rune = (Rune)(point.Item1 % 2 == point.Item2 % 2 ? '▓' : '▒'); } else { rune = (Rune)'█'; } AddRune (point.Item1, point.Item2, rune); } } /// protected override bool OnMouseEvent (MouseEvent mouseEvent) { if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { if (IsForegroundPoint (mouseEvent.Position.X, mouseEvent.Position.Y)) { ClickedInForeground (); } else if (IsBackgroundPoint (mouseEvent.Position.X, mouseEvent.Position.Y)) { ClickedInBackground (); } } return base.OnMouseEvent (mouseEvent); } private bool IsForegroundPoint (int x, int y) { return ForegroundPoints.Contains ((x, y)); } private bool IsBackgroundPoint (int x, int y) { return BackgroundPoints.Contains ((x, y)); } private void ClickedInBackground () { if (LineDrawing.PromptForColor ("Background", Value.Background, out Color newColor)) { Value = new (Value.Foreground, newColor); SetNeedsDisplay (); } } private void ClickedInForeground () { if (LineDrawing.PromptForColor ("Foreground", Value.Foreground, out Color newColor)) { Value = new (newColor, Value.Background); SetNeedsDisplay (); } } }