GUI桌面环境

This commit is contained in:
2026-03-01 17:03:49 +08:00
parent 545f40cf95
commit f0a9223520
162 changed files with 9170 additions and 135 deletions

9
Gui/UILib/Alignment.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace CMLeonOS.Gui.UILib
{
internal enum Alignment
{
Start,
Middle,
End
}
}

View File

@@ -0,0 +1,84 @@
using CMLeonOS.Gui;
namespace CMLeonOS.UILib.Animations
{
/// <summary>
/// A window animation.
/// </summary>
internal abstract class Animation
{
/// <summary>
/// The easing type of the animation.
/// </summary>
internal EasingType EasingType { get; set; } = EasingType.Sine;
/// <summary>
/// The direction of the easing of the animation.
/// </summary>
internal EasingDirection EasingDirection { get; set; } = EasingDirection.Out;
/// <summary>
/// The duration of the animation.
/// </summary>
internal int Duration { get; set; } = 60;
/// <summary>
/// How many frames of the animation have been completed.
/// </summary>
internal int Position { get; set; } = 0;
/// <summary>
/// If the animation has finished.
/// </summary>
internal bool Finished
{
get
{
return Position >= Duration;
}
}
/// <summary>
/// The window associated with the animation.
/// </summary>
internal Window Window { get; set; }
/// <summary>
/// Advance the animation by one frame.
/// </summary>
/// <returns>Whether or not the animation is now finished.</returns>
internal abstract bool Advance();
private int? timerId { get; set; } = null;
/// <summary>
/// Start the animation.
/// </summary>
internal void Start()
{
if (timerId == null)
{
timerId = Cosmos.HAL.Global.PIT.RegisterTimer(new Cosmos.HAL.PIT.PITTimer(() =>
{
Advance();
if (Finished)
{
Stop();
}
}, (ulong)((1000d /* ms */ / 60d) * 1e+6d /* ms -> ns */ ), true));
}
}
/// <summary>
/// Stop the animation.
/// </summary>
internal void Stop()
{
if (timerId != null)
{
Cosmos.HAL.Global.PIT.UnregisterTimer((int)timerId);
timerId = null;
}
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
namespace CMLeonOS.UILib.Animations
{
/// <summary>
/// Easing utilities for animations.
/// </summary>
internal static class Easing
{
/// <summary>
/// Calculate the value of an easing function.
/// </summary>
/// <param name="t">The absolute progress of the animation, from 0 to 1.</param>
/// <param name="type">The type of the easing function.</param>
/// <param name="direction">The direction of the easing function.</param>
/// <returns>The value of the easing function at the given progress.</returns>
/// <exception cref="ArgumentOutOfRangeException">An exception is thrown if the progress is out of range.</exception>
/// <exception cref="ArgumentException">An exception is thrown if the type or direction is ininvalid.</exception>
internal static double Ease(double t, EasingType type, EasingDirection direction)
{
if (t < 0 || t > 1) throw new ArgumentOutOfRangeException();
switch (type)
{
case EasingType.Linear:
return t;
case EasingType.Sine:
switch (direction)
{
case EasingDirection.In:
return 1 - Math.Cos(t * Math.PI / 2);
case EasingDirection.Out:
return Math.Sin(t * Math.PI / 2);
case EasingDirection.InOut:
return -0.5 * (Math.Cos(Math.PI * t) - 1);
default:
throw new ArgumentException("Unknown easing direction.");
}
default:
throw new ArgumentException("Unknown easing type.");
}
}
/// <summary>
/// Linearly interpolate between two values.
/// </summary>
/// <param name="x">The first value.</param>
/// <param name="y">The second value.</param>
/// <param name="z">The value of the interpolation.</param>
/// <returns>The interpolated value.</returns>
internal static double Lerp(double x, double y, double z)
{
return x * (1 - z) + y * z;
}
}
}

View File

@@ -0,0 +1,23 @@
namespace CMLeonOS.UILib.Animations
{
/// <summary>
/// Defines the direction of an easing type.
/// </summary>
internal enum EasingDirection
{
/// <summary>
/// Starts the animation slowly, and finishes at full speed.
/// </summary>
In,
/// <summary>
/// Starts the animation at full speed, and finishes slowly.
/// </summary>
Out,
/// <summary>
/// Starts the animation slowly, reaches full speed at the middle, and finishes slowly.
/// </summary>
InOut
}
}

View File

@@ -0,0 +1,18 @@
namespace CMLeonOS.UILib.Animations
{
/// <summary>
/// Describes the speed of motion over time in an animation.
/// </summary>
internal enum EasingType
{
/// <summary>
/// Linear easing.
/// </summary>
Linear,
/// <summary>
/// Sinusoidal easing.
/// </summary>
Sine
}
}

View File

@@ -0,0 +1,64 @@
using CMLeonOS.Gui;
using CMLeonOS.Gui.UILib;
using System;
using System.Drawing;
namespace CMLeonOS.UILib.Animations
{
/// <summary>
/// An animation that moves or resizes a window.
/// </summary>
internal class MovementAnimation : Animation
{
/// <summary>
/// Initialise the animation.
/// </summary>
/// <param name="window">The window associated with the animation.</param>
/// <param name="to">The goal of the animation.</param>
internal MovementAnimation(Window window)
{
Window = window;
From = new Rectangle(window.X, window.Y, window.Width, window.Height);
}
/// <summary>
/// The starting rectangle of the animation.
/// </summary>
internal Rectangle From;
/// <summary>
/// The goal rectangle of the animation.
/// </summary>
internal Rectangle To;
internal override bool Advance()
{
if (From.IsEmpty || To.IsEmpty) throw new Exception("The From or To value of this MovementAnimation is empty.");
Position++;
if (Position == Duration)
{
Window.MoveAndResize(To.X, To.Y, To.Width, To.Height);
if (Window is Control control)
{
control.Render();
}
}
else
{
double t = Easing.Ease(Position / (double)Duration, EasingType, EasingDirection);
Rectangle current = new Rectangle(
(int)Easing.Lerp(From.X, To.X, t),
(int)Easing.Lerp(From.Y, To.Y, t),
(int)Easing.Lerp(From.Width, To.Width, t),
(int)Easing.Lerp(From.Height, To.Height, t)
);
Window.MoveAndResize(current.X, current.Y, current.Width, current.Height);
if (Window is Control control)
{
control.Render();
}
}
return Finished;
}
}
}

243
Gui/UILib/AppWindow.cs Normal file
View File

@@ -0,0 +1,243 @@
using Cosmos.System;
using Cosmos.System.Graphics;
using CMLeonOS;
using CMLeonOS.Gui.ShellComponents.Dock;
using CMLeonOS.Gui.SmoothMono;
using System;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class AppWindow : Window
{
internal AppWindow(Process process, int x, int y, int width, int height) : base(process, x, y, width, height)
{
wm = ProcessManager.GetProcess<WindowManager>();
decorationWindow = new Window(process, 0, -titlebarHeight, width, titlebarHeight);
wm.AddWindow(decorationWindow);
decorationWindow.RelativeTo = this;
decorationWindow.OnClick = DecorationClicked;
decorationWindow.OnDown = DecorationDown;
Icon = defaultAppIconBitmap;
RenderDecoration();
}
[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.Close.bmp")]
private static byte[] closeBytes;
private static Bitmap closeBitmap = new Bitmap(closeBytes);
[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.Maximise.bmp")]
private static byte[] maximiseBytes;
private static Bitmap maximiseBitmap = new Bitmap(maximiseBytes);
/*[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.Minimise.bmp")]
private static byte[] minimiseBytes;
private static Bitmap minimiseBitmap = new Bitmap(minimiseBytes);*/
[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.Restore.bmp")]
private static byte[] restoreBytes;
private static Bitmap restoreBitmap = new Bitmap(restoreBytes);
[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.AppIcons.Default.bmp")]
private static byte[] defaultAppIconBytes;
private static Bitmap defaultAppIconBitmap = new Bitmap(defaultAppIconBytes);
internal Action Closing;
private Bitmap _icon;
private Bitmap _smallIcon;
internal Bitmap Icon
{
get
{
return _icon;
}
set
{
_icon = value;
_smallIcon = _icon.Resize(20, 20);
RenderDecoration();
ProcessManager.GetProcess<Dock>()?.UpdateWindows();
}
}
private string _title = "Window";
internal string Title
{
get
{
return _title;
}
set
{
_title = value;
RenderDecoration();
}
}
private bool _canResize = false;
internal bool CanResize
{
get
{
return _canResize;
}
set
{
_canResize = value;
RenderDecoration();
}
}
private bool _canClose = true;
internal bool CanClose
{
get
{
return _canClose;
}
set
{
_canClose = value;
RenderDecoration();
}
}
private bool _canMove = true;
internal bool CanMove
{
get
{
return _canMove;
}
set
{
_canMove = value;
RenderDecoration();
}
}
private const int titlebarHeight = 24;
private Window decorationWindow;
private WindowManager wm;
private bool maximised = false;
private int originalX;
private int originalY;
private int originalWidth;
private int originalHeight;
private void DecorationClicked(int x, int y)
{
if (x >= Width - titlebarHeight && _canClose)
{
// Close.
Closing?.Invoke();
wm.RemoveWindow(this);
}
else if (x >= Width - (titlebarHeight * (_canClose ? 2 : 1)) && _canResize)
{
// Maximise / restore.
if (maximised)
{
maximised = false;
MoveAndResize(originalX, originalY, originalWidth, originalHeight, sendWMEvent: false);
decorationWindow.Resize(originalWidth, titlebarHeight, sendWMEvent: false);
UserResized?.Invoke();
ProcessManager.GetProcess<WindowManager>().RerenderAll();
}
else
{
maximised = true;
var taskbar = ProcessManager.GetProcess<ShellComponents.Taskbar>();
int taskbarHeight = taskbar.GetTaskbarHeight();
var dock = ProcessManager.GetProcess<ShellComponents.Dock.Dock>();
int dockHeight = dock.GetDockHeight();
originalX = X;
originalY = Y;
originalWidth = Width;
originalHeight = Height;
MoveAndResize(
0,
taskbarHeight + titlebarHeight,
(int)wm.ScreenWidth,
(int)wm.ScreenHeight - titlebarHeight - taskbarHeight - dockHeight,
sendWMEvent: false
);
decorationWindow.Resize((int)wm.ScreenWidth, titlebarHeight, sendWMEvent: false);
UserResized?.Invoke();
ProcessManager.GetProcess<WindowManager>().RerenderAll();
}
RenderDecoration();
}
}
private void DecorationDown(int x, int y)
{
int buttonSpace = 0;
if (_canClose)
{
buttonSpace += titlebarHeight;
}
if (_canResize)
{
buttonSpace += titlebarHeight;
}
if (x >= Width - buttonSpace || maximised || !_canMove) return;
uint startMouseX = MouseManager.X;
uint startMouseY = MouseManager.Y;
int startWindowX = X;
int startWindowY = Y;
while (MouseManager.MouseState == MouseState.Left)
{
X = (int)(startWindowX + (MouseManager.X - startMouseX));
Y = (int)(startWindowY + (MouseManager.Y - startMouseY));
ProcessManager.Yield();
}
}
private void RenderDecoration()
{
decorationWindow.Clear(Color.FromArgb(56, 56, 71));
if (_smallIcon != null)
{
decorationWindow.DrawImageAlpha(_smallIcon, 2, 2);
}
decorationWindow.DrawString(Title, Color.White, (Width / 2) - ((FontData.Width * Title.Length) / 2), 4);
if (_canClose)
{
decorationWindow.DrawImageAlpha(closeBitmap, Width - titlebarHeight, 0);
}
if (_canResize)
{
decorationWindow.DrawImageAlpha(maximised ? restoreBitmap : maximiseBitmap, Width - (titlebarHeight * (_canClose ? 2 : 1)), 0);
}
wm.Update(decorationWindow);
}
}
}

133
Gui/UILib/Button.cs Normal file
View File

@@ -0,0 +1,133 @@
using Cosmos.System.Graphics;
using System;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class Button : Control
{
public Button(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height)
{
}
internal enum ButtonImageLocation
{
AboveText,
Left
}
private string _text = "Button";
internal string Text
{
get
{
return _text;
}
set
{
_text = value;
Render();
}
}
private ButtonImageLocation _imageLocation = ButtonImageLocation.AboveText;
internal ButtonImageLocation ImageLocation
{
get
{
return _imageLocation;
}
set
{
_imageLocation = value;
Render();
}
}
private Color _background = Color.FromArgb(48, 48, 48);
internal Color Background
{
get
{
return _background;
}
set
{
_background = value;
Render();
}
}
private Color _foreground = Color.White;
internal Color Foreground
{
get
{
return _foreground;
}
set
{
_foreground = value;
Render();
}
}
private Color _border = Color.Black;
internal Color Border
{
get
{
return _border;
}
set
{
_border = value;
Render();
}
}
private Bitmap _image;
internal Bitmap Image
{
get
{
return _image;
}
set
{
_image = value;
Render();
}
}
internal override void Render()
{
Clear(Background);
if (_image != null)
{
switch (_imageLocation)
{
case ButtonImageLocation.Left:
DrawImageAlpha(_image, (int)((Width / 2) - ((8 / 2) * Text.Length) - 8 - _image.Width), (int)((Height / 2) - (_image.Height / 2)));
DrawString(Text, Foreground, (Width / 2) - ((8 / 2) * Text.Length), (Height / 2) - (16 / 2));
break;
case ButtonImageLocation.AboveText:
DrawImageAlpha(_image, (int)((Width / 2) - (_image.Width / 2)), (int)((Height / 2) - (_image.Height / 2)));
DrawString(Text, Foreground, (Width / 2) - (4 * Text.Length), Height - 16);
break;
default:
throw new Exception("Unrecognised image location in button.");
}
}
else
{
DrawString(Text, Foreground, (Width / 2) - (4 * Text.Length), (Height / 2) - 8);
}
DrawRectangle(0, 0, Width, Height, Border);
WM.Update(this);
}
}
}

204
Gui/UILib/Calendar.cs Normal file
View File

@@ -0,0 +1,204 @@
using System;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class Calendar : Control
{
public Calendar(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height)
{
}
internal void SetCalendar(int year, int month)
{
_year = year;
_month = month;
Render();
}
private int _year = DateTime.Now.Year;
internal int Year
{
get
{
return _year;
}
set
{
_year = value;
Render();
}
}
private int _month = DateTime.Now.Month;
internal int Month
{
get
{
return _month;
}
set
{
_month = value;
Render();
}
}
private Color _background = Color.White;
internal Color Background
{
get
{
return _background;
}
set
{
_background = value;
Render();
}
}
private Color _foreground = Color.Black;
internal Color Foreground
{
get
{
return _foreground;
}
set
{
_foreground = value;
Render();
}
}
private Color _weekendForeground = Color.Red;
internal Color WeekendForeground
{
get
{
return _weekendForeground;
}
set
{
_weekendForeground = value;
Render();
}
}
private Color _todayBackground = Color.LightGray;
internal Color TodayBackground
{
get
{
return _todayBackground;
}
set
{
_todayBackground = value;
Render();
}
}
private readonly string[] weekdaysShort = new string[]
{
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun"
};
private readonly string[] monthsLong = new string[]
{
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
private int GetWeekdayIndex(DayOfWeek dayOfWeek)
{
return dayOfWeek switch
{
DayOfWeek.Monday => 0,
DayOfWeek.Tuesday => 1,
DayOfWeek.Wednesday => 2,
DayOfWeek.Thursday => 3,
DayOfWeek.Friday => 4,
DayOfWeek.Saturday => 5,
DayOfWeek.Sunday => 6,
_ => throw new Exception("Invalid DayOfWeek.")
};
}
private const int cellPadding = 4;
internal override void Render()
{
Clear(_background);
DateTime now = DateTime.Now;
int daysInMonth = DateTime.DaysInMonth(_year, _month);
int startingWeekday = GetWeekdayIndex(new DateTime(_year, _month, 1).DayOfWeek);
int headerHeight = 68;
Rectangle availableSpace = new Rectangle(0, headerHeight, Width, Height - headerHeight);
int cellWidth = availableSpace.Width / 7;
int cellHeight = availableSpace.Height / 5;
/* Header */
string title = $"{monthsLong[_month - 1]} {_year}";
DrawString(title, _foreground, (Width / 2) - ((title.Length * 8) / 2), 12);
for (int i = 0; i < 7; i++)
{
string weekday = weekdaysShort[i];
DrawString(weekday, _foreground, (i * cellWidth) + ((cellWidth / 2) - weekday.Length * (8 / 2) / 2), 40);
}
/* Days */
int cellX = startingWeekday;
int cellY = 0;
for (int i = 1; i <= daysInMonth; i++)
{
if (cellX > 6)
{
cellX = 0;
cellY++;
}
string str = i.ToString();
bool weekend = cellX >= 5;
int cellWindowX = availableSpace.X + (cellX * cellWidth);
int cellWindowY = availableSpace.Y + (cellY * cellHeight);
int textWindowX = (cellWindowX + cellWidth) - (8 * str.Length) - cellPadding;
int textWindowY = cellWindowY + cellPadding;
if (_year == now.Year && _month == now.Month && i == now.Day)
{
DrawFilledRectangle(cellWindowX, cellWindowY, cellWidth, cellHeight, _todayBackground);
}
DrawString(str, weekend ? _weekendForeground : _foreground, textWindowX, textWindowY);
cellX++;
}
WM.Update(this);
}
}
}

115
Gui/UILib/CheckBox.cs Normal file
View File

@@ -0,0 +1,115 @@
using Cosmos.System.Graphics;
using System;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class CheckBox : Control
{
public CheckBox(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height)
{
OnClick = CheckBoxClicked;
}
[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.Check.bmp")]
private static byte[] checkBytes;
private static Bitmap checkBitmap = new Bitmap(checkBytes);
internal Action CheckBoxChecked;
internal Action CheckBoxUnchecked;
internal Action<bool> CheckBoxChanged;
private const int iconSize = 16;
private bool _checked = false;
internal bool Checked
{
get
{
return _checked;
}
set
{
_checked = value;
if (_checked)
{
CheckBoxChecked?.Invoke();
}
else
{
CheckBoxUnchecked?.Invoke();
}
CheckBoxChanged?.Invoke(_checked);
Render();
}
}
private string _text = "CheckBox";
internal string Text
{
get
{
return _text;
}
set
{
_text = value;
Render();
}
}
private Color _background = Color.White;
internal Color Background
{
get
{
return _background;
}
set
{
_background = value;
Render();
}
}
private Color _foreground = Color.Black;
internal Color Foreground
{
get
{
return _foreground;
}
set
{
_foreground = value;
Render();
}
}
private void CheckBoxClicked(int x, int y)
{
Checked = !Checked;
}
internal override void Render()
{
Clear(Background);
int iconX = 0;
int iconY = (Height / 2) - (iconSize / 2);
int textX = iconSize + 8;
int textY = (Height / 2) - (16 / 2);
DrawFilledRectangle(iconX, iconY, iconSize, iconSize, Color.LightGray);
if (_checked)
{
DrawImageAlpha(checkBitmap, iconX, iconY);
}
DrawString(Text, _foreground, textX, textY);
WM.Update(this);
}
}
}

14
Gui/UILib/Control.cs Normal file
View File

@@ -0,0 +1,14 @@
namespace CMLeonOS.Gui.UILib
{
internal abstract class Control : Window
{
internal Control(Window parent, int x, int y, int width, int height) : base(parent.Process, x, y, width, height)
{
RelativeTo = parent;
Render();
}
internal abstract void Render();
}
}

17
Gui/UILib/Extensions.cs Normal file
View File

@@ -0,0 +1,17 @@
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal static class Extensions
{
internal static float GetLuminance(this Color color)
{
return (float)((color.R * 0.2126) + (color.G * 0.7152) + (color.B * 0.0722));
}
internal static Color GetForegroundColour(this Color color)
{
return color.GetLuminance() < 140 ? Color.White : Color.Black;
}
}
}

61
Gui/UILib/ImageBlock.cs Normal file
View File

@@ -0,0 +1,61 @@
using Cosmos.System.Graphics;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class ImageBlock : Control
{
public ImageBlock(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height)
{
}
private Bitmap _image;
internal Bitmap Image
{
get
{
return _image;
}
set
{
_image = value;
Render();
}
}
private bool _alpha = false;
internal bool Alpha
{
get
{
return _alpha;
}
set
{
_alpha = value;
Render();
}
}
internal override void Render()
{
if (_image == null)
{
Clear(Color.Gray);
WM.Update(this);
return;
}
if (_alpha)
{
DrawImageAlpha(_image, 0, 0);
}
else
{
DrawImage(_image, 0, 0);
}
WM.Update(this);
}
}
}

58
Gui/UILib/MessageBox.cs Normal file
View File

@@ -0,0 +1,58 @@
using CMLeonOS;
using System;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class MessageBox
{
internal MessageBox(Process process, string title, string message)
{
this.Process = process;
Title = title;
Message = message;
}
protected const int Padding = 12;
internal void Show()
{
WindowManager wm = ProcessManager.GetProcess<WindowManager>();
int longestLineLength = 0;
foreach (string line in Message.Split('\n'))
{
longestLineLength = Math.Max(longestLineLength, line.Length);
}
int width = Math.Max(192, (Padding * 2) + (8 * longestLineLength));
int height = 128 + ((Message.Split('\n').Length - 1) * 16);
AppWindow window = new AppWindow(Process, (int)((wm.ScreenWidth / 2) - (height / 2)), (int)((wm.ScreenWidth / 2) - (width / 2)), width, height);
window.Title = Title;
wm.AddWindow(window);
window.Clear(Color.LightGray);
window.DrawFilledRectangle(0, window.Height - (Padding * 2) - 20, window.Width, (Padding * 2) + 20, Color.Gray);
window.DrawString(Message, Color.Black, Padding, Padding);
Button ok = new Button(window, window.Width - 80 - Padding, window.Height - 20 - Padding, 80, 20);
ok.Text = "OK";
ok.OnClick = (int x, int y) =>
{
wm.RemoveWindow(window);
};
wm.AddWindow(ok);
wm.Update(window);
ProcessManager.GetProcess<Sound.SoundService>().PlaySystemSound(Sound.SystemSound.Alert);
}
internal Process Process { get; private set; }
internal string Title { get; private set; }
internal string Message { get; private set; }
}
}

60
Gui/UILib/PromptBox.cs Normal file
View File

@@ -0,0 +1,60 @@
using CMLeonOS;
using CMLeonOS.Gui.SmoothMono;
using System;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class PromptBox : MessageBox
{
internal PromptBox(Process process, string title, string message, string placeholder, Action<string> submitted) : base(process, title, message)
{
Placeholder = placeholder;
Submitted = submitted;
}
internal void Show()
{
WindowManager wm = ProcessManager.GetProcess<WindowManager>();
int longestLineLength = 0;
foreach (string line in Message.Split('\n'))
{
longestLineLength = Math.Max(longestLineLength, line.Length);
}
int width = Math.Max(256, (Padding * 2) + (8 * longestLineLength));
int height = 128 + ((Message.Split('\n').Length - 1) * 16);
AppWindow window = new AppWindow(Process, (int)((wm.ScreenWidth / 2) - (height / 2)), (int)((wm.ScreenWidth / 2) - (width / 2)), width, height);
window.Title = Title;
wm.AddWindow(window);
window.Clear(Color.LightGray);
window.DrawFilledRectangle(0, window.Height - (Padding * 2) - 20, window.Width, (Padding * 2) + 20, Color.Gray);
window.DrawString(Message, Color.Black, Padding, Padding);
TextBox textBox = new TextBox(window, Padding, Padding + FontData.Height + 8, 192, 20);
textBox.PlaceholderText = Placeholder;
wm.AddWindow(textBox);
Button ok = new Button(window, window.Width - 80 - Padding, window.Height - 20 - Padding, 80, 20);
ok.Text = "OK";
ok.OnClick = (int x, int y) =>
{
wm.RemoveWindow(window);
Submitted.Invoke(textBox.Text);
};
wm.AddWindow(ok);
wm.Update(window);
ProcessManager.GetProcess<Sound.SoundService>().PlaySystemSound(Sound.SystemSound.Alert);
}
internal Action<string> Submitted { get; private set; }
internal string Placeholder { get; private set; }
}
}

195
Gui/UILib/RangeSlider.cs Normal file
View File

@@ -0,0 +1,195 @@
using Cosmos.System;
using CMLeonOS;
using CMLeonOS.Gui.SmoothMono;
using CMLeonOS.Utils;
using System;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class RangeSlider : Control
{
public RangeSlider(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height)
{
OnDown = RangeSliderDown;
}
public RangeSlider(Window parent, int x, int y, int width, int height, float min, float value, float max) : base(parent, x, y, width, height)
{
OnDown = RangeSliderDown;
_minimum = min;
_value = value;
_maximum = max;
Render();
}
private Color _background = Color.White;
internal Color Background
{
get
{
return _background;
}
set
{
_background = value;
Render();
}
}
private Color _foreground = Color.Gray;
internal Color Foreground
{
get
{
return _foreground;
}
set
{
_foreground = value;
Render();
}
}
private float _minimum = 0;
internal float Minimum
{
get
{
return _minimum;
}
set
{
if (_minimum != value)
{
_minimum = value;
Render();
}
}
}
private float _value = 50;
internal float Value
{
get
{
return _value;
}
set
{
if (_value != value)
{
_value = value;
Render();
Changed?.Invoke(value);
}
}
}
private float _maximum = 100;
internal float Maximum
{
get
{
return _maximum;
}
set
{
if (_maximum != value)
{
_maximum = value;
Render();
}
}
}
private bool _rangeLabels = true;
internal bool RangeLabels
{
get
{
return _rangeLabels;
}
set
{
if (_rangeLabels != value)
{
_rangeLabels = value;
Render();
}
}
}
internal Action<float> Changed { get; set; }
private bool held = false;
private static int slotHeight = 3;
private static int sliderHeight = 15;
private static int sliderWidth = 5;
private void RangeSliderDown(int x, int y)
{
held = true;
Render();
}
internal override void Render()
{
if (held && MouseManager.MouseState != MouseState.Left)
{
held = false;
}
if (held)
{
float relativeX = (float)(MouseManager.X - ScreenX);
float clamped = Math.Clamp(relativeX, 0, Width - sliderWidth);
//DrawString(clamped.ToString(), Color.Red, 0, 0);
Value = (float)clamped.Map(0, Width - sliderWidth, (float)_minimum, (float)_maximum);
WM.UpdateQueue.Enqueue(this);
}
Clear(Background);
int slotY;
int sliderY;
if (_rangeLabels)
{
slotY = (sliderHeight / 2) - (slotHeight / 2);
sliderY = 0;
}
else
{
slotY = (Height / 2) - (slotHeight / 2);
sliderY = (Height / 2) - (sliderHeight / 2);
}
// Slot
DrawFilledRectangle(0, slotY, Width, slotHeight, Color.FromArgb(168, 168, 168));
// Slider
DrawFilledRectangle(
(int)(_value.Map((float)_minimum, (float)_maximum, 0, Width - sliderWidth)),
sliderY,
sliderWidth,
sliderHeight,
held ? Color.FromArgb(0, 71, 112) : Color.FromArgb(0, 115, 186)
);
if (_rangeLabels)
{
DrawString(_minimum.ToString(), Foreground, 0, Height - FontData.Height);
DrawString(_maximum.ToString(), Foreground, Width - (FontData.Width * _maximum.ToString().Length), Height - FontData.Height);
}
WM.Update(this);
}
}
}

93
Gui/UILib/ShortcutBar.cs Normal file
View File

@@ -0,0 +1,93 @@
using CMLeonOS.Gui.SmoothMono;
using System.Collections.Generic;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class ShortcutBar : Control
{
public ShortcutBar(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height)
{
OnClick = ShortcutBarClick;
}
internal List<ShortcutBarCell> Cells { get; set; } = new List<ShortcutBarCell>();
private Color _background = Color.LightGray;
internal Color Background
{
get
{
return _background;
}
set
{
_background = value;
Render();
}
}
private Color _foreground = Color.Black;
internal Color Foreground
{
get
{
return _foreground;
}
set
{
_foreground = value;
Render();
}
}
private int _cellPadding = 10;
internal int CellPadding
{
get
{
return _cellPadding;
}
set
{
_cellPadding = value;
Render();
}
}
private void ShortcutBarClick(int x, int y)
{
int cellEndX = 0;
foreach (var cell in Cells)
{
cellEndX += (_cellPadding * 2) + (FontData.Width * cell.Text.Length);
if (x < cellEndX)
{
cell.OnClick?.Invoke();
return;
}
}
}
internal override void Render()
{
Clear(Background);
int cellX = 0;
for (int i = 0; i < Cells.Count; i++)
{
ShortcutBarCell cell = Cells[i];
Rectangle cellRect = new Rectangle(cellX, 0, Width, Height);
int textX = cellRect.X + _cellPadding;
int textY = cellRect.Y + (cellRect.Height / 2) - (FontData.Height / 2);
DrawString(cell.Text, Foreground, textX, textY);
cellX += (_cellPadding * 2) + (FontData.Width * cell.Text.Length);
}
WM.Update(this);
}
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace CMLeonOS.Gui.UILib
{
internal class ShortcutBarCell
{
internal ShortcutBarCell(string text, Action onClick)
{
Text = text;
OnClick = onClick;
}
internal string Text { get; set; } = string.Empty;
internal Action OnClick { get; set; }
}
}

118
Gui/UILib/Switch.cs Normal file
View File

@@ -0,0 +1,118 @@
using Cosmos.System;
using Cosmos.System.Graphics;
using System;
namespace CMLeonOS.Gui.UILib
{
internal class Switch : CheckBox
{
public Switch(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height)
{
OnDown = SwitchDown;
OnClick = null;
}
[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.SwitchOff.bmp")]
private static byte[] offBytes;
private static Bitmap offBitmap = new Bitmap(offBytes);
[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.SwitchOn.bmp")]
private static byte[] onBytes;
private static Bitmap onBitmap = new Bitmap(onBytes);
[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.SwitchKnob.bmp")]
private static byte[] knobBytes;
private static Bitmap knobBitmap = new Bitmap(knobBytes);
private const int maximumToggleDrag = 4;
private int lastMouseX = 0;
private int totalDragged = 0;
private bool held = false;
private void SwitchDown(int x, int y)
{
lastMouseX = (int)MouseManager.X;
totalDragged = 0;
held = true;
Render();
}
private void Release()
{
held = false;
if (totalDragged <= maximumToggleDrag)
{
// Interpret as a toggle.
Checked = !Checked;
}
else
{
// Interpret as a drag rather than a toggle,
// setting the Checked state based on where
// the switch knob is.
Checked = knobX >= (offBitmap.Width / 2) - (knobBitmap.Width / 2);
}
}
private double knobX = -1;
private double knobGoal = 0;
internal override void Render()
{
knobGoal = (int)(Checked ? offBitmap.Width - knobBitmap.Width : 0);
if (held && MouseManager.MouseState != MouseState.Left)
{
Release();
}
if (held)
{
int diff = (int)(MouseManager.X - lastMouseX);
lastMouseX = (int)MouseManager.X;
totalDragged += Math.Abs(diff);
knobX = Math.Clamp(knobX + diff, 0, offBitmap.Width - knobBitmap.Width);
WM.UpdateQueue.Enqueue(this);
}
else
{
double oldKnobX = knobX;
if (knobX == -1)
{
knobX = knobGoal;
}
else
{
double diff = knobGoal - knobX;
double move = diff / 8d;
knobX += move;
}
if (Math.Abs(knobX - oldKnobX) < 0.25)
{
knobX = knobGoal;
}
else
{
WM.UpdateQueue.Enqueue(this);
}
}
Clear(Background);
int switchX = 0;
int switchY = (Height / 2) - ((int)offBitmap.Height / 2);
int textX = (int)(offBitmap.Width + 8);
int textY = (Height / 2) - (16 / 2);
DrawImageAlpha(Checked ? onBitmap : offBitmap, switchX, switchY);
DrawImageAlpha(knobBitmap, (int)knobX, switchY);
DrawString(Text, Foreground, textX, textY);
WM.Update(this);
}
}
}

393
Gui/UILib/Table.cs Normal file
View File

@@ -0,0 +1,393 @@
using Cosmos.System;
using Cosmos.System.Graphics;
using CMLeonOS.Gui.SmoothMono;
using System;
using System.Collections.Generic;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class Table : Control
{
public Table(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height)
{
OnDown = TableDown;
}
[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.ScrollbarUp.bmp")]
private static byte[] scrollbarUpBytes;
private static Bitmap scrollbarUpBitmap = new Bitmap(scrollbarUpBytes);
[IL2CPU.API.Attribs.ManifestResourceStream(ResourceName = "CMLeonOS.Gui.Resources.ScrollbarDown.bmp")]
private static byte[] scrollbarDownBytes;
private static Bitmap scrollbarDownBitmap = new Bitmap(scrollbarDownBytes);
internal List<TableCell> Cells { get; set; } = new List<TableCell>();
internal Action<int> TableCellSelected { get; set; }
internal bool AllowDeselection { get; set; } = true;
internal bool AllowSelection { get; set; } = true;
private double scrollY = 0;
private bool dragging = false;
private int lastDragY = 0;
private int _selectedCellIndex = -1;
internal int SelectedCellIndex
{
get
{
return _selectedCellIndex;
}
set
{
if (_selectedCellIndex != value)
{
_selectedCellIndex = value;
Render();
}
}
}
private int _scrollbarThickness = 20;
internal int ScrollbarThickness
{
get
{
return _scrollbarThickness;
}
set
{
_scrollbarThickness = value;
Render();
}
}
private Color _background = Color.LightGray;
internal Color Background
{
get
{
return _background;
}
set
{
_background = value;
Render();
}
}
private Color _foreground = Color.Black;
internal Color Foreground
{
get
{
return _foreground;
}
set
{
_foreground = value;
Render();
}
}
private Color _border = Color.Gray;
internal Color Border
{
get
{
return _border;
}
set
{
_border = value;
Render();
}
}
private Color _selectedBackground = Color.FromArgb(221, 246, 255);
internal Color SelectedBackground
{
get
{
return _selectedBackground;
}
set
{
_selectedBackground = value;
Render();
}
}
private Color _selectedForeground = Color.Black;
internal Color SelectedForeground
{
get
{
return _selectedForeground;
}
set
{
_selectedForeground = value;
Render();
}
}
private Color _selectedBorder = Color.FromArgb(126, 205, 234);
internal Color SelectedBorder
{
get
{
return _selectedBorder;
}
set
{
_selectedBorder = value;
Render();
}
}
private int _cellHeight = 20;
internal int CellHeight
{
get
{
return _cellHeight;
}
set
{
_cellHeight = value;
Render();
}
}
private Alignment _textAlignment = Alignment.Start;
internal Alignment TextAlignment
{
get
{
return _textAlignment;
}
set
{
_textAlignment = value;
Render();
}
}
private void TableDown(int x, int y)
{
if ((CanScrollUp || CanScrollDown) && x >= (Width - _scrollbarThickness))
{
int allCellsHeight = Cells.Count * CellHeight;
if (y < _scrollbarThickness && CanScrollUp)
{
scrollY = Math.Max(0, scrollY - CellHeight);
Render();
}
if (y >= Height - _scrollbarThickness && CanScrollDown)
{
scrollY = Math.Min(allCellsHeight - Height, scrollY + CellHeight);
Render();
}
if (y < Height - _scrollbarThickness && y >= _scrollbarThickness)
{
dragging = true;
lastDragY = (int)MouseManager.Y;
Render();
}
return;
}
int scrollAdjustedY = (int)(y + scrollY);
if (scrollAdjustedY < 0 || scrollAdjustedY > _cellHeight * Cells.Count)
{
if (AllowDeselection)
{
SelectedCellIndex = -1;
}
return;
}
if (AllowSelection)
{
SelectedCellIndex = scrollAdjustedY / _cellHeight;
TableCellSelected?.Invoke(_selectedCellIndex);
}
}
private bool CanScrollUp
{
get
{
return scrollY > 0;
}
}
private bool CanScrollDown
{
get
{
int allCellsHeight = Cells.Count * CellHeight;
return (scrollY < 0) || ((allCellsHeight > Height) && (scrollY < (allCellsHeight - Height)));
}
}
private void RenderScrollbar()
{
if (CanScrollUp || CanScrollDown)
{
/* Background */
DrawFilledRectangle(Width - _scrollbarThickness, 0, _scrollbarThickness, Height, _border);
/* Track */
int trackAvailableHeight = Height - (ScrollbarThickness * 2);
double trackSize = (double)Height / (double)(Cells.Count * CellHeight);
double trackProgress = (double)scrollY / (double)((Cells.Count * CellHeight) - Height);
int trackY = (int)(_scrollbarThickness + (((double)trackAvailableHeight - ((double)trackAvailableHeight * trackSize)) * trackProgress));
// Border
DrawFilledRectangle(Width - _scrollbarThickness, 0, _scrollbarThickness, Height, _border);
// Background
DrawFilledRectangle(Width - _scrollbarThickness + 1, trackY + 1, _scrollbarThickness - 2, (int)(trackSize * trackAvailableHeight) - 2, _background);
/* Up arrow */
// Border
DrawFilledRectangle(Width - _scrollbarThickness, 0, _scrollbarThickness, _scrollbarThickness, _border);
// Background
DrawFilledRectangle(Width - _scrollbarThickness + 1, 1, _scrollbarThickness - 2, _scrollbarThickness - 2, CanScrollUp ? _background : _border);
DrawImageAlpha(scrollbarUpBitmap, (int)((Width - _scrollbarThickness) + ((_scrollbarThickness / 2) - (scrollbarUpBitmap.Width / 2))), (int)((_scrollbarThickness / 2) - (scrollbarUpBitmap.Height / 2)));
/* Down arrow */
// Border
DrawFilledRectangle(Width - _scrollbarThickness, Height - _scrollbarThickness, _scrollbarThickness, _scrollbarThickness, _border);
// Background
DrawFilledRectangle(Width - _scrollbarThickness + 1, Height - _scrollbarThickness + 1, _scrollbarThickness - 2, _scrollbarThickness - 2, CanScrollDown ? _background : _border);
DrawImageAlpha(scrollbarDownBitmap, (int)((Width - _scrollbarThickness) + ((_scrollbarThickness / 2) - (scrollbarUpBitmap.Width / 2))), (int)((Height - _scrollbarThickness) + ((_scrollbarThickness / 2) - (scrollbarUpBitmap.Height / 2))));
}
}
internal void ScrollToTop()
{
scrollY = 0;
Render();
}
internal void ScrollToBottom()
{
int allCellsHeight = Cells.Count * CellHeight;
if (allCellsHeight > Height)
{
scrollY = allCellsHeight - Height;
}
else
{
scrollY = 0;
}
Render();
}
internal override void Render()
{
int scrollMax = (Cells.Count * CellHeight) - Height;
if (dragging)
{
scrollY += (int)(MouseManager.Y - lastDragY);
lastDragY = (int)MouseManager.Y;
if (MouseManager.MouseState != MouseState.Left)
{
dragging = false;
}
WM.UpdateQueue.Enqueue(this);
}
else if (scrollY < 0 || scrollY > scrollMax)
{
double oldScrollY = scrollY;
double move;
if (scrollY > 0)
{
move = (scrollMax - scrollY) / 8d;
}
else
{
move = (-scrollY) / 8d;
}
scrollY += move;
if (Math.Abs(scrollY - oldScrollY) > 0.05)
{
WM.UpdateQueue.Enqueue(this);
}
}
Clear(Background);
for (int i = 0; i < Cells.Count; i++)
{
TableCell cell = Cells[i];
bool selected = _selectedCellIndex == i;
Rectangle cellRect = new Rectangle(0, (int)((i * _cellHeight) - scrollY), Width, _cellHeight);
if (cellRect.Y < -cellRect.Height || cellRect.Y > Height)
{
continue;
}
if (cell.BackgroundColourOverride != null)
{
DrawFilledRectangle(cellRect.X, cellRect.Y, cellRect.Width, cellRect.Height, (Color)cell.BackgroundColourOverride);
}
else
{
// Border.
DrawFilledRectangle(cellRect.X, cellRect.Y, cellRect.Width, cellRect.Height, selected ? _selectedBorder : _border);
// Background.
DrawFilledRectangle(cellRect.X + 1, cellRect.Y + 1, cellRect.Width - 2, cellRect.Height - 2, selected ? _selectedBackground : _background);
}
int textX;
switch (_textAlignment)
{
case Alignment.Start:
textX = cellRect.X + (cell.Image != null ? (CellHeight - FontData.Height) / 2 : 0);
break;
case Alignment.Middle:
textX = cellRect.X + (cellRect.Width / 2) - (cell.Text.Length * FontData.Width / 2);
break;
case Alignment.End:
textX = cellRect.X + cellRect.Width - (cell.Text.Length * FontData.Width);
break;
default:
throw new Exception("Invalid Table alignment!");
}
int textY = cellRect.Y + (cellRect.Height / 2) - (16 / 2);
if (cell.Image != null)
{
textX += (int)cell.Image.Width;
DrawImageAlpha(cell.Image, cellRect.X, (int)(cellRect.Y + (cellRect.Height / 2) - (cell.Image.Height / 2)));
}
if (cell.ForegroundColourOverride != null)
{
DrawString(cell.Text, (Color)cell.ForegroundColourOverride, textX, textY);
}
else
{
DrawString(cell.Text, selected ? SelectedForeground : Foreground, textX, textY);
}
}
//DrawString($"{scrollY.ToString()} {dragging.ToString()} {scrollMax.ToString()}", Color.Red, 0, 0);
RenderScrollbar();
WM.Update(this);
}
}
}

42
Gui/UILib/TableCell.cs Normal file
View File

@@ -0,0 +1,42 @@
using Cosmos.System.Graphics;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class TableCell
{
internal TableCell(string text)
{
Text = text;
}
internal TableCell(string text, object tag)
{
Text = text;
Tag = tag;
}
internal TableCell(Bitmap image, string text)
{
Image = image;
Text = text;
}
internal TableCell(Bitmap image, string text, object tag)
{
Image = image;
Text = text;
Tag = tag;
}
internal Bitmap Image { get; set; }
internal string Text { get; set; } = string.Empty;
internal object Tag { get; set; }
internal Color? BackgroundColourOverride { get; set; } = null;
internal Color? ForegroundColourOverride { get; set; } = null;
}
}

124
Gui/UILib/TextBlock.cs Normal file
View File

@@ -0,0 +1,124 @@
using System;
using System.Drawing;
namespace CMLeonOS.Gui.UILib
{
internal class TextBlock : Control
{
public TextBlock(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height)
{
}
private string _text = "TextBlock";
internal string Text
{
get
{
return _text;
}
set
{
_text = value;
Render();
}
}
private Color _background = Color.White;
internal Color Background
{
get
{
return _background;
}
set
{
_background = value;
Render();
}
}
private Color _foreground = Color.Black;
internal Color Foreground
{
get
{
return _foreground;
}
set
{
_foreground = value;
Render();
}
}
private Alignment _horizontalAlignment = Alignment.Start;
internal Alignment HorizontalAlignment
{
get
{
return _horizontalAlignment;
}
set
{
_horizontalAlignment = value;
Render();
}
}
private Alignment _verticalAlignment = Alignment.Start;
internal Alignment VerticalAlignment
{
get
{
return _verticalAlignment;
}
set
{
_verticalAlignment = value;
Render();
}
}
internal override void Render()
{
Clear(Background);
int textX;
int textY;
switch (HorizontalAlignment)
{
case Alignment.Start:
textX = 0;
break;
case Alignment.Middle:
textX = (Width / 2) - (8 * Text.Length / 2);
break;
case Alignment.End:
textX = Width - (8 * Text.Length);
break;
default:
throw new Exception("Invalid horizontal alignment.");
}
switch (VerticalAlignment)
{
case Alignment.Start:
textY = 0;
break;
case Alignment.Middle:
textY = (Height / 2) - (16 / 2);
break;
case Alignment.End:
textY = Height - 16;
break;
default:
throw new Exception("Invalid vertical alignment.");
}
DrawString(Text, Foreground, textX, textY);
WM.Update(this);
}
}
}

375
Gui/UILib/TextBox.cs Normal file
View File

@@ -0,0 +1,375 @@
using Cosmos.System;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
namespace CMLeonOS.Gui.UILib
{
internal class TextBox : Control
{
public TextBox(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height)
{
OnDown = TextBoxDown;
OnKeyPressed = TextBoxKeyPressed;
OnUnfocused = TextBoxUnfocused;
}
internal Action Submitted;
internal Action Changed;
internal string Text
{
get
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < lines.Count; i++)
{
builder.Append(lines[i]);
if (i != lines.Count - 1)
{
builder.AppendLine();
}
}
return builder.ToString();
}
set
{
lines = value.Split('\n').ToList();
caretLine = -1;
caretCol = 0;
MarkAllLines();
Render();
}
}
private string _placeholderText = string.Empty;
internal string PlaceholderText
{
get
{
return _placeholderText;
}
set
{
_placeholderText = value;
Render();
}
}
internal bool ReadOnly { get; set; } = false;
internal bool MultiLine { get; set; } = false;
internal bool Shield { get; set; } = false;
private Color _background = Color.White;
internal Color Background
{
get
{
return _background;
}
set
{
_background = value;
Clear(_background);
MarkAllLines();
Render();
}
}
private Color _foreground = Color.Black;
internal Color Foreground
{
get
{
return _foreground;
}
set
{
_foreground = value;
MarkAllLines();
Render();
}
}
private Color _placeholderForeground = Color.Gray;
internal Color PlaceholderForeground
{
get
{
return _placeholderForeground;
}
set
{
_placeholderForeground = value;
Render();
}
}
private void MoveCaret(int line, int col)
{
if (caretLine == line && caretCol == col) return;
MarkLine(caretLine);
caretLine = Math.Clamp(line, 0, lines.Count - 1);
caretCol = Math.Clamp(col, 0, lines[caretLine].Length);
MarkLine(caretLine);
Render();
}
private void TextBoxDown(int x, int y)
{
MoveCaret((y + scrollY) / fontHeight, ((x + scrollX) + (fontWidth / 2)) / fontWidth);
}
private void TextBoxUnfocused()
{
MarkLine(caretLine);
caretLine = -1;
caretCol = 0;
Render();
}
private void AutoScroll()
{
if (caretLine == -1) return;
if (scrollY + Height < (caretLine + 1) * fontHeight)
{
// Scroll up.
scrollY = ((caretLine + 1) * fontHeight) - Height;
MarkAllLines();
}
if (caretLine * fontHeight < scrollY)
{
// Scroll down.
scrollY = caretLine * fontHeight;
MarkAllLines();
}
if (scrollX + Width < (caretCol + 1) * fontWidth)
{
// Scroll right.
scrollX = ((caretCol + 1) * fontWidth) - Width;
MarkAllLines();
}
if (caretCol * fontWidth < scrollX)
{
// Scroll left.
scrollX = caretCol * fontWidth;
MarkAllLines();
}
}
private void TextBoxKeyPressed(KeyEvent key)
{
if (caretLine == -1 || ReadOnly) return;
switch (key.Key)
{
case ConsoleKeyEx.LeftArrow:
if (caretCol == 0)
{
if (caretLine == 0) return;
caretLine--;
caretCol = lines[caretLine].Length;
MarkLine(caretLine);
MarkLine(caretLine + 1);
}
else
{
caretCol--;
MarkLine(caretLine);
}
break;
case ConsoleKeyEx.RightArrow:
if (caretCol == lines[caretLine].Length)
{
if (caretLine == lines.Count - 1) return;
caretLine++;
caretCol = 0;
MarkLine(caretLine - 1);
MarkLine(caretLine);
}
else
{
caretCol++;
MarkLine(caretLine);
}
break;
case ConsoleKeyEx.UpArrow:
if (caretLine == 0) return;
caretLine--;
caretCol = Math.Min(lines[caretLine].Length, caretCol);
MarkLine(caretLine);
MarkLine(caretLine + 1);
break;
case ConsoleKeyEx.DownArrow:
if (caretLine == lines.Count - 1) return;
caretLine++;
caretCol = Math.Min(lines[caretLine].Length, caretCol);
MarkLine(caretLine - 1);
MarkLine(caretLine);
break;
case ConsoleKeyEx.Enter:
if (!MultiLine)
{
Submitted?.Invoke();
caretLine = -1;
caretCol = 0;
MarkAllLines();
break;
}
lines.Insert(caretLine + 1, lines[caretLine].Substring(caretCol));
lines[caretLine] = lines[caretLine].Substring(0, caretCol);
caretLine++;
caretCol = 0;
MarkLine(caretLine - 1);
MarkLine(caretLine);
Changed?.Invoke();
break;
case ConsoleKeyEx.Backspace:
if (caretCol == 0)
{
if (caretLine == 0) return;
caretLine--;
caretCol = lines[caretLine].Length;
lines[caretLine] += lines[caretLine + 1];
lines.RemoveAt(caretLine + 1);
MarkLine(caretLine);
MarkLine(caretLine + 1);
Changed?.Invoke();
}
else
{
lines[caretLine] = lines[caretLine].Remove(caretCol - 1, 1);
caretCol--;
MarkLine(caretLine);
Changed?.Invoke();
}
break;
default:
lines[caretLine] = lines[caretLine].Insert(caretCol, key.KeyChar.ToString());
caretCol++;
MarkLine(caretLine);
Changed?.Invoke();
break;
}
Render();
}
private void MarkLine(int lineNum)
{
if (markedLinesBegin == -1)
{
markedLinesBegin = lineNum;
}
else
{
markedLinesBegin = Math.Min(markedLinesBegin, lineNum);
}
if (markedLinesEnd == -1)
{
markedLinesEnd = lineNum;
}
else
{
markedLinesEnd = Math.Max(markedLinesEnd, lineNum);
}
}
internal void MarkAllLines()
{
markedLinesBegin = 0;
markedLinesEnd = lines.Count - 1;
}
private List<string> lines = new List<string>() { string.Empty };
private int markedLinesBegin = 0;
private int markedLinesEnd = 0;
private const int fontWidth = 8;
private const int fontHeight = 16;
private int caretLine = -1;
private int caretCol = 0;
private int scrollX = 0;
private int scrollY = 0;
internal override void Render()
{
if (Text == string.Empty)
{
Clear(_background);
DrawRectangle(0, 0, Width, Height, Color.Gray);
DrawString(PlaceholderText, PlaceholderForeground, 0, 0);
if (caretLine == 0)
{
DrawVerticalLine(fontHeight, 1, 0, Foreground);
}
WM.Update(this);
return;
}
if (markedLinesBegin == -1 || markedLinesEnd == -1) return;
AutoScroll();
for (int i = markedLinesBegin; i <= markedLinesEnd; i++)
{
int lineY = (i * fontHeight) - scrollY;
if (lineY < 0) continue;
if (lineY > Height) break;
DrawFilledRectangle(0, lineY, Width, fontHeight, Background);
if (i < lines.Count)
{
DrawString(Shield ? new string('*', lines[i].Length) : lines[i], Foreground, -scrollX, lineY);
if (caretLine == i)
{
DrawVerticalLine(fontHeight, ((caretCol * fontWidth) - scrollX) + 1, (caretLine * fontHeight) - scrollY, Foreground);
}
}
}
markedLinesBegin = -1;
markedLinesEnd = -1;
DrawRectangle(0, 0, Width, Height, Color.Gray);
WM.Update(this);
}
}
}