mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
Refactored `IDriver` interface: - Updated `SetScreenSize` to remove `NotSupportedException`. - Clarified `Refresh` method as internal and updated documentation for `SizeChanged` event.
476 lines
14 KiB
C#
476 lines
14 KiB
C#
using System.Text;
|
|
|
|
namespace UICatalog.Scenarios;
|
|
|
|
public interface ITool
|
|
{
|
|
void OnMouseEvent (DrawingArea area, MouseEventArgs mouseEvent);
|
|
}
|
|
|
|
internal class DrawLineTool : ITool
|
|
{
|
|
private StraightLine _currentLine;
|
|
public LineStyle LineStyle { get; set; } = LineStyle.Single;
|
|
|
|
/// <inheritdoc/>
|
|
public void OnMouseEvent (DrawingArea area, MouseEventArgs 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.SetNeedsDraw ();
|
|
}
|
|
}
|
|
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.SetNeedsDraw ();
|
|
}
|
|
}
|
|
|
|
mouseEvent.Handled = true;
|
|
}
|
|
}
|
|
|
|
[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.SetCurrentAttribute (e);
|
|
tools.SetStyle += b => canvas.CurrentTool = new DrawLineTool { LineStyle = b };
|
|
tools.AddLayer += () => canvas.AddLayer ();
|
|
|
|
win.Add (canvas);
|
|
win.Add (tools);
|
|
tools.CurrentColor = canvas.GetAttributeForRole (VisualRole.Normal);
|
|
canvas.CurrentAttribute = tools.CurrentColor;
|
|
|
|
win.KeyDown += (s, e) => { e.Handled = canvas.NewKeyDownEvent (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.Handled = true;
|
|
Application.RequestStop ();
|
|
};
|
|
|
|
var btnCancel = new Button
|
|
{
|
|
X = Pos.Center () + 5,
|
|
Y = 4,
|
|
Text = "Cancel",
|
|
Width = Dim.Auto ()
|
|
};
|
|
|
|
btnCancel.Accepting += (s, e) =>
|
|
{
|
|
e.Handled = 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 OptionSelector<LineStyle> _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), AssignHotKeys = true
|
|
};
|
|
_stylePicker.ValueChanged += (s, a) =>
|
|
{
|
|
if (a.Value is { })
|
|
{
|
|
SetStyle?.Invoke ((LineStyle)a.Value);
|
|
}
|
|
};
|
|
_stylePicker.Value = LineStyle.Single;
|
|
|
|
_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<Attribute> ColorChanged;
|
|
public event Action<LineStyle> SetStyle;
|
|
|
|
private void ToolsView_Initialized (object sender, EventArgs e)
|
|
{
|
|
Width = Math.Max (_colors.Frame.Width, _stylePicker.Frame.Width) + GetAdornmentsThickness ().Horizontal;
|
|
Height = _colors.Frame.Height + _stylePicker.Frame.Height + _addLayerBtn.Frame.Height + GetAdornmentsThickness ().Vertical;
|
|
}
|
|
}
|
|
|
|
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 (); }
|
|
|
|
protected override bool OnDrawingContent (DrawContext context)
|
|
{
|
|
foreach (LineCanvas canvas in Layers)
|
|
{
|
|
foreach (KeyValuePair<Point, Cell?> c in canvas.GetCellMap ())
|
|
{
|
|
if (c.Value is { })
|
|
{
|
|
SetCurrentAttribute (c.Value.Value.Attribute ?? GetAttributeForRole (VisualRole.Normal));
|
|
|
|
// TODO: #2616 - Support combining sequences that don't normalize
|
|
AddStr (c.Key.X, c.Key.Y, c.Value.Value.Grapheme);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: This is a hack to work around overlapped views not drawing correctly.
|
|
// without this the toolbox disappears
|
|
SuperView?.SetNeedsLayout ();
|
|
|
|
return true;
|
|
}
|
|
|
|
//// BUGBUG: Why is this not handled by a key binding???
|
|
protected 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);
|
|
SetNeedsDraw ();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (e.KeyCode == (KeyCode.Y | KeyCode.CtrlMask))
|
|
{
|
|
if (_undoHistory.Any ())
|
|
{
|
|
StraightLine pop = _undoHistory.Pop ();
|
|
CurrentLayer.AddLine (pop);
|
|
SetNeedsDraw ();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
|
|
{
|
|
CurrentTool.OnMouseEvent (this, mouseEvent);
|
|
|
|
return mouseEvent.Handled;
|
|
}
|
|
|
|
internal void AddLayer ()
|
|
{
|
|
CurrentLayer = new ();
|
|
Layers.Add (CurrentLayer);
|
|
}
|
|
|
|
internal void SetCurrentAttribute (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/>
|
|
protected override bool OnDrawingContent (DrawContext context)
|
|
{
|
|
Color fg = Value.Foreground;
|
|
Color bg = Value.Background;
|
|
|
|
bool isTransparentFg = fg == GetAttributeForRole (VisualRole.Normal).Background;
|
|
bool isTransparentBg = bg == GetAttributeForRole (VisualRole.Normal).Background;
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override bool OnMouseEvent (MouseEventArgs 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 ();
|
|
}
|
|
|
|
mouseEvent.Handled = true;
|
|
}
|
|
|
|
return mouseEvent.Handled;
|
|
}
|
|
|
|
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, Value.Style);
|
|
SetNeedsDraw ();
|
|
}
|
|
}
|
|
|
|
private void ClickedInForeground ()
|
|
{
|
|
if (LineDrawing.PromptForColor ("Foreground", Value.Foreground, out Color newColor))
|
|
{
|
|
Value = new (newColor, Value.Background);
|
|
SetNeedsDraw ();
|
|
}
|
|
}
|
|
}
|