-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDrawer.cs
231 lines (192 loc) · 8.35 KB
/
Drawer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// Developed by Bulat Bagaviev (@sunnyyssh).
// This file is licensed to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace Sunnyyssh.ConsoleUI;
/// <summary>
/// The options of <see cref="Drawer"/>. They give opportuninty to custom drawing process.
/// </summary>
/// <param name="DefaultBackground">The default background color</param>
/// <param name="DefaultForeground">The default foreground color</param>
/// <param name="BorderConflictsAllowed">Indicates if it's expected to throw an exception on trying to draw outside the buffer.</param>
/// <param name="RequestsNotRunningAllowed">
/// Indicates if it's expected to throw an exception when new request is enqueued when Drawer is not running.
/// </param>
internal record DrawerOptions(Color DefaultBackground, Color DefaultForeground, bool BorderConflictsAllowed,
bool RequestsNotRunningAllowed = false)
{
/// <summary>
/// The width of drawing field. Fully ignored when running OS is not windows.
/// </summary>
public int? Width { get; init; }
/// <summary>
/// The height of drawing field. Fully ignored when running OS is not windows.
/// </summary>
public int? Height { get; init; }
}
/// <summary>
/// Provides all drawing process in the specialized drawing thread.
/// </summary>
internal class Drawer
{
// All drawing directly in console is incapsulated in DrawerPal class
private DrawerPal _drawerPal;
// When Drawer.Stop() method is invoked this should be cancelled
// to cancel actual drawing operations and make incoming requests be not drawn.
private readonly CancellationTokenSource _cancellation = new();
// When request to draw InternalDrawState occurs it is enqueued in this queue.
// This collection gives an oportunity to wait for requests when the actual queue is empty.
private readonly RequestsQueue<DrawState> _drawRequestsQueue = new();
private readonly DrawerOptions _options;
/// <summary>
/// Indicates if Drawer has been already started.
/// </summary>
public bool IsRunning { get; private set; }
/// <summary>
/// The width of buffer where drawer can actually draw.
/// </summary>
public int BufferWidth => _drawerPal.BufferWidth;
/// <summary>
/// The height of buffer where drawer can actually draw.
/// </summary>
public int BufferHeight => _drawerPal.BufferHeight;
public static int WindowWidth => DrawerPal.WindowWidth;
public static int WindowHeight => DrawerPal.WindowHeight;
/// <summary>
/// Enqueues a request to draw the state.
/// It's dequeued and drawn when current drawing iteration ends
/// or immediately if it's not drawing at the moment.
/// </summary>
/// <param name="drawState">The state to enqueue to draw.</param>
public void EnqueueRequest(DrawState drawState)
{
ArgumentNullException.ThrowIfNull(drawState, nameof(drawState));
if (!IsRunning && !_options.RequestsNotRunningAllowed)
{
throw new DrawingException("Drawer is not running.");
}
ArgumentNullException.ThrowIfNull(drawState, nameof(drawState));
_drawRequestsQueue.Enqueue(drawState);
}
/// <summary>
/// Starts drawing process in the another thread.
/// </summary>
/// <exception cref="DrawingException"></exception>
public void Start()
{
if (IsRunning)
throw new DrawingException("It's already running.");
// All drawing iterations must be performed in the another thread
// not to hold the calling thread while drawing.
Thread drawingThread = new Thread(() =>
{
RunWithCancellation(_cancellation.Token);
})
{
// false because this thread should hold the app running.
IsBackground = false,
};
drawingThread.Start();
IsRunning = true;
}
// In dependence on options different DrawerPal instances can be initialized.
// The realization may differ depending on platform, specific options etc.
[MemberNotNull(nameof(_drawerPal))]
private void InitializeDrawerPal(DrawerOptions options)
{
//If application is running on windows then DrawerPal implementation is ought to be specified for windows.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_drawerPal = new WindowsDrawerPal(
options.DefaultBackground,
options.DefaultForeground,
options.BorderConflictsAllowed,
options.Width,
options.Height);
return;
}
//If there are no reason to use specific implementations of DrawerPal
//the default DrawerPal implementations should be used.
_drawerPal = new DrawerPal(
options.DefaultBackground,
options.DefaultForeground,
options.BorderConflictsAllowed); // Default DrawerPal.
}
// Method invoked in the drawing thread.
// It loops the drawing iterations while it's cancelled
// and waits for requests ot cancellation when no requests are in queue.
private void RunWithCancellation(CancellationToken cancellationToken)
{
// Drawing default background.
FillConsoleWithColor(_options.DefaultBackground, cancellationToken);
// Making DrawerPal instance handle starting drawing process.
_drawerPal.OnStart();
// Looping while it's not cancelled.
while (!cancellationToken.IsCancellationRequested)
{
DrawRequests(cancellationToken);
// Waiting for the request or the cancellation.
_drawRequestsQueue.WaitForRequests();
}
}
// Fills console with color.
private void FillConsoleWithColor(Color color, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return;
_drawerPal.Clear();
int height = _drawerPal.BufferHeight;
int width = _drawerPal.BufferWidth;
// Creating the state of the whole console one-colored
var fillingState = new DrawStateBuilder(width, height)
.Fill(color)
.ToDrawState();
// It's important not to enqueue this request but directly draw it
// because it can overlap already enqueued requests, what is fully unexpected.
DrawSingleRequest(fillingState, cancellationToken);
}
public void Stop()
{
if (!IsRunning)
throw new DrawingException("It's not running.");
// It's necessary to cancel before exiting waiting
// because otherwise it goes to the another iteration and waits again before it canceles
_cancellation.Cancel();
// If drawing thread is waiting for requests
// we force stopping waiting
// and make the drawing thread finish its work.
// Look void RunWithCancellation(CancellationToken)
_drawRequestsQueue.ForceStopWaiting();
IsRunning = false;
}
// Dequeues and draws all enqueued requests.
private void DrawRequests(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return;
if (_drawRequestsQueue.IsEmpty)
return;
// Dequeueing requests into ordered collection.
var allRequests = _drawRequestsQueue.DequeueAll();
// All requests should be combined into one.
// What's important, later states are expected to overlap the earlier ones.
var combinedRequest = DrawState.Combine(allRequests);
DrawSingleRequest(combinedRequest, cancellationToken);
}
private void DrawSingleRequest(DrawState drawState, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return;
_drawerPal.DrawSingleRequest(drawState, cancellationToken);
}
/// <summary>
/// Creates an instance of <see cref="Drawer"/> with specified options.
/// </summary>
/// <param name="options">Specific drawing options.</param>
public Drawer(DrawerOptions options)
{
ArgumentNullException.ThrowIfNull(options, nameof(options));
InitializeDrawerPal(options);
_options = options;
}
}