Files
Terminal.Gui/UICatalog/Scenarios/LineDrawing.cs
Thomas Nind 38f84f7424 Fixes #2800 - Color picker (supporting hsl, hsv and rgb) (#3604)
* Readonly HSL view

* Make it possible to move between bars by moving to subview

* Basically working and with mouse support

* Fix HSL to work properly with double values instead of color matching

* Fix Value on ColorPicker to match HSL values

* Fix color spectrum

* Add Swatch and better sync with text box

* Work on jitter

* ColorPicker HSL working

* More keybindings

* Add ColorModel

* Support both HSL and HSV

* Add RGB

* Better mouse handling

* WIP: AttributeView and integrate into LineDrawing
(does not currently work properly)

* Fix color picking

* Add concept of an ITool

* Add ColorPickerStyle

* Fix selected cell rendering

* Add first test for ColorPicker2

* Add more RGB tests

* Improve ColorPicker2 setup process

* Tests and fixes for keyboard changing value R

* Fix margin on bars when no textfields

* Add mouse test

* Add tests for with text field

* Add more tests and fix bug sync component text field change with hex text field

(WIP - failing tests)

* Fix tests and fix clicking in a bar label area possibly not selecting

* Move AttributeView to LineDrawing and adjust to have a 'transparent pattern' too

* Render triangle in dark gray if background is black

* Add ColorChanged event

* Resharper Cleanup

* Xml comments and public/private adjustments

* Explore replacing diagram test with fragile Subview diving

* Migrate ColorPicker_DefaultBoot to sub asserts

* Port other tests

* Replace ColorPicker with new view

* Fix ColorPicker size to match scenarios size assumptions

* Split to separate files and ignore invalid test for ColorPicker

* Ignore also in mouse version of AllViews_Enter_Leave_Events

* Remove bool _updating from ColorPicker

Now instead we are more selective about what we update when and do so deterministically

* Typo fix

* Fix ReSharper bad renames in comments for "Value"

* Refactor to single implementation of 'prompt for color' logic

- Now called PromptForColor
- Shared by LineDrawing and ProgressBarStyles scenarios

* Sum runes instead of Length

* Hide ColorBar and SetValueWithoutRaisingEvent from public API

* Move ColorEventArgs to Drawing folder

* Move ColorModel to Drawing folder

* First try at Dim.Auto for ColorPicker

* Remove explicit width/height setting in most scenarios

* Remove explicit heights

* Fixed build/test issues.
Illustrated test best practice.

* WIP: Start working on test changes and add new options to ColorPickers scenario (Color Model and show textfields).

* Fix for R indicator arrow sometimes 'falling off' the drawn area.

* Add nullable enable

* Test fixes and refactor for avoiding Begin

* Make ColorEventArgs inherit from EventArgs<Color>

* Fix Dispose not being called on bars when switching color models

* Remove 'oldColor' from test now it is not supported

* Add initial stab at ColorPickerStyle.ShowName

* Use AppendAutocomplete for color names

* Implemented resoruce based colorname resolver

* Update GetTextField to support getting the color names field
Change style setting to ShowColorName

* Color name updates when navigating away from the named color

* Restore old color picker as ColorPicker16

* Add test that shows 'Save as' is currently considered a named color ><

* Fix GetW3CColorNames

* Removed dupe colors

* Revert to old color pickers

* Nullability question marks for everyone!

---------

Co-authored-by: Tig <tig@users.noreply.github.com>
2024-08-22 17:53:04 -06:00

457 lines
13 KiB
C#

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;
/// <inheritdoc/>
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,
Height = 7
};
var btnOk = new Button
{
X = Pos.Center () - 5,
Y = 4,
Text = "Ok",
Width = Dim.Auto (),
IsDefault = true
};
btnOk.Accept += (s, e) =>
{
accept = true;
e.Handled = true;
Application.RequestStop ();
};
var btnCancel = new Button
{
X = Pos.Center () + 5,
Y = 4,
Text = "Cancel",
Width = Dim.Auto ()
};
btnCancel.Accept += (s, e) =>
{
e.Handled = true;
Application.RequestStop ();
};
d.Add (btnOk);
d.Add (btnCancel);
/* Does not work
d.AddButton (btnOk);
d.AddButton (btnCancel);
*/
var cp = new ColorPicker
{
SelectedColor = current,
Width = Dim.Fill ()
};
d.Add (cp);
Application.Run (d);
d.Dispose ();
newColor = 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.Accept += (s, a) => AddLayer?.Invoke ();
Add (_colors, _stylePicker, _addLayerBtn);
}
public event EventHandler<Attribute> ColorChanged;
public event Action<LineStyle> 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<LineCanvas> Layers = new ();
private readonly Stack<StraightLine> _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<Point, Cell?> 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<Attribute> 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;
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
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 ();
}
}
}